From 3ee74039bd89339a9b38f6a07caaa3af542c1d3c Mon Sep 17 00:00:00 2001 From: Julien Coolen Date: Fri, 15 May 2026 19:18:03 +0200 Subject: [PATCH 01/19] feat(transcript): add Keccak challenge sampling Add a Keccak256 transcript hash implementation and update the challenge sampling path to use a single Keccak squeeze with big-endian scalar encoding. --- proofs/Cargo.toml | 6 + proofs/src/transcript/implementors.rs | 164 +++++++++++++++++++++++++- 2 files changed, 168 insertions(+), 2 deletions(-) diff --git a/proofs/Cargo.toml b/proofs/Cargo.toml index 2fe066d08..3933975de 100644 --- a/proofs/Cargo.toml +++ b/proofs/Cargo.toml @@ -39,6 +39,7 @@ midnight-curves = "0.2.0" rand_core = { workspace = true, features = ["getrandom"] } tracing = { workspace = true } blake2b_simd = { workspace = true } +sha3 = { workspace = true, optional = true } rand_chacha = { workspace = true } itertools = "0.14" serde = { workspace = true } @@ -75,6 +76,11 @@ circuit-params = [] derive_serde = ["midnight-curves/serde"] dev-curves = ["midnight-curves/serde"] +# Enables a Keccak256-based `TranscriptHash` implementation (from the `sha3` +# crate) along with the associated `Hashable`/`Sampleable` implementations +# for BLS12-381 (and BN256 under `dev-curves`) curve types. +keccak-transcript = ["dep:sha3"] + # This feature truncates challenges to half the size of the scalar field. truncated-challenges = [] diff --git a/proofs/src/transcript/implementors.rs b/proofs/src/transcript/implementors.rs index 37a397c3e..d4e6b5a74 100644 --- a/proofs/src/transcript/implementors.rs +++ b/proofs/src/transcript/implementors.rs @@ -5,11 +5,40 @@ use ff::{FromUniformBytes, PrimeField}; use group::GroupEncoding; #[cfg(feature = "dev-curves")] use midnight_curves::bn256::{Fr, G1}; +#[cfg(feature = "keccak-transcript")] +use num_bigint::BigUint; +#[cfg(feature = "keccak-transcript")] +use sha3::{Digest, Keccak256}; +#[cfg(feature = "keccak-transcript")] use crate::transcript::{ Hashable, Sampleable, TranscriptHash, BLAKE2B_PREFIX_CHALLENGE, BLAKE2B_PREFIX_COMMON, }; +#[cfg(feature = "keccak-transcript")] +fn sample_keccak_digest_be_mod_r(hash_output: Vec) -> F { + assert_eq!(hash_output.len(), 32); + + let modulus = if let Some(hex) = F::MODULUS.strip_prefix("0x") { + BigUint::parse_bytes(hex.as_bytes(), 16) + } else { + BigUint::parse_bytes(F::MODULUS.as_bytes(), 10) + } + .expect("PrimeField::MODULUS must parse as an integer"); + + let reduced = BigUint::from_bytes_be(&hash_output) % modulus; + let reduced_le = reduced.to_bytes_le(); + + let mut repr = F::Repr::default(); + assert!( + reduced_le.len() <= repr.as_ref().len(), + "reduced Keccak challenge does not fit in field repr" + ); + repr.as_mut()[..reduced_le.len()].copy_from_slice(&reduced_le); + + Option::from(F::from_repr(repr)).expect("reduced Keccak challenge must be canonical") +} + impl TranscriptHash for Blake2bState { type Input = Vec; type Output = Vec; @@ -49,7 +78,7 @@ impl>> Hashable for u32 { impl Hashable for G1 { /// Converts it to compressed form in bytes fn to_input(&self) -> Vec { - Hashable::to_bytes(self) + Hashable::::to_bytes(self) } fn to_bytes(&self) -> Vec { @@ -103,7 +132,7 @@ impl Sampleable for Fr { impl Hashable for midnight_curves::G1Projective { /// Converts it to compressed form in bytes fn to_input(&self) -> Vec { - Hashable::to_bytes(self) + Hashable::::to_bytes(self) } fn to_bytes(&self) -> Vec { @@ -148,3 +177,134 @@ impl Sampleable for midnight_curves::Fq { midnight_curves::Fq::from_uniform_bytes(&bytes) } } + +// ///////////////////////////////////////////////////////////////////// +// /// Implementation of TranscriptHash for Keccak256 // +// ///////////////////////////////////////////////////////////////////// + +#[cfg(feature = "keccak-transcript")] +impl TranscriptHash for Keccak256 { + type Input = Vec; + type Output = Vec; + + fn init() -> Self { + Keccak256::new() + } + + fn absorb(&mut self, input: &Self::Input) { + self.update(input); + } + + fn squeeze(&mut self) -> Self::Output { + // Keccak256 produces a 32-byte digest. For Fiat-Shamir challenges we + // sample from that digest directly as a big-endian integer modulo the + // scalar-field modulus. + let out = self.clone().finalize().to_vec(); + + // Re-seed the state with the squeezed challenge so that subsequent + // absorb/squeeze calls depend on the challenge we just produced. + let mut new_hasher = Keccak256::new(); + new_hasher.update(&out); + *self = new_hasher; + + out + } +} + +#[cfg(all(feature = "dev-curves", feature = "keccak-transcript"))] +impl Hashable for G1 { + fn to_input(&self) -> Vec { + Hashable::::to_bytes(self) + } + + fn to_bytes(&self) -> Vec { + ::to_bytes(self).as_ref().to_vec() + } + + fn read(buffer: &mut impl Read) -> io::Result { + let mut bytes = ::Repr::default(); + + buffer.read_exact(bytes.as_mut())?; + + Option::from(Self::from_bytes(&bytes)) + .ok_or_else(|| io::Error::other("Invalid BN point encoding in proof")) + } +} + +#[cfg(all(feature = "dev-curves", feature = "keccak-transcript"))] +impl Hashable for Fr { + fn to_input(&self) -> Vec { + let mut bytes = self.to_bytes().to_vec(); + bytes.reverse(); + bytes + } + + fn to_bytes(&self) -> Vec { + self.to_bytes().to_vec() + } + + fn read(buffer: &mut impl Read) -> io::Result { + let mut bytes = ::Repr::default(); + + buffer.read_exact(bytes.as_mut())?; + + Option::from(Self::from_repr(bytes)) + .ok_or_else(|| io::Error::other("Invalid BN scalar encoding in proof")) + } +} + +#[cfg(all(feature = "dev-curves", feature = "keccak-transcript"))] +impl Sampleable for Fr { + fn sample(hash_output: Vec) -> Self { + sample_keccak_digest_be_mod_r(hash_output) + } +} + +#[cfg(feature = "keccak-transcript")] +impl Hashable for midnight_curves::G1Projective { + fn to_input(&self) -> Vec { + Hashable::::to_bytes(self) + } + + fn to_bytes(&self) -> Vec { + ::to_bytes(self).as_ref().to_vec() + } + + fn read(buffer: &mut impl Read) -> io::Result { + let mut bytes = ::Repr::default(); + + buffer.read_exact(bytes.as_mut())?; + + Option::from(Self::from_bytes(&bytes)) + .ok_or_else(|| io::Error::other("Invalid BLS12-381 point encoding in proof")) + } +} + +#[cfg(feature = "keccak-transcript")] +impl Hashable for midnight_curves::Fq { + fn to_input(&self) -> Vec { + let mut bytes = self.to_repr().to_vec(); + bytes.reverse(); + bytes + } + + fn to_bytes(&self) -> Vec { + self.to_repr().to_vec() + } + + fn read(buffer: &mut impl Read) -> io::Result { + let mut bytes = ::Repr::default(); + + buffer.read_exact(bytes.as_mut())?; + + Option::from(Self::from_repr(bytes)) + .ok_or_else(|| io::Error::other("Invalid BLS12-381 scalar encoding in proof")) + } +} + +#[cfg(feature = "keccak-transcript")] +impl Sampleable for midnight_curves::Fq { + fn sample(hash_output: Vec) -> Self { + sample_keccak_digest_be_mod_r(hash_output) + } +} From 02a2f73526ff7232a2089ae029fb5c98c7f19ee4 Mon Sep 17 00:00:00 2001 From: Julien Coolen Date: Fri, 15 May 2026 19:18:09 +0200 Subject: [PATCH 02/19] feat(ivc): add Keccak final verifier fixtures Switch the stdlib Poseidon example to the Keccak transcript path, add final-step IVC prove/verify coverage, and publish Solidity verifier fixtures for replay. --- aggregation/Cargo.toml | 10 + aggregation/src/ivc/prover.rs | 107 ++ aggregation/src/ivc/verifier.rs | 64 + aggregation/tests/ivc_keccak_final.rs | 209 ++++ proofs/solidity-verifier/.gitignore | 2 + .../fixtures/ivc/instance.bin | Bin 0 -> 3528 bytes .../solidity-verifier/fixtures/ivc/proof.bin | Bin 0 -> 9824 bytes .../fixtures/ivc/rust_trace.json | 1068 +++++++++++++++++ .../fixtures/ivc/solidity_trace.json | 1 + proofs/solidity-verifier/fixtures/ivc/vk.bin | Bin 0 -> 66731 bytes .../poseidon/adversarial_fixtures.bin | Bin 0 -> 38369 bytes .../fixtures/poseidon/algebra_fixtures.bin | Bin 0 -> 2272 bytes .../fixtures/poseidon/decomp_pairs.bin | Bin 0 -> 704 bytes .../poseidon/evals_signature_fixture.bin | Bin 0 -> 64 bytes .../fixtures/poseidon/gate_eval_fixture.bin | Bin 0 -> 2813 bytes .../poseidon/identity_g1_compressed.bin | Bin 0 -> 48 bytes .../fixtures/poseidon/instance.be | 1 + .../poseidon/lagrange_aux_fixture.bin | Bin 0 -> 256 bytes .../fixtures/poseidon/pairing_fixture.bin | 1 + .../fixtures/poseidon/proof.bin | Bin 0 -> 4752 bytes .../poseidon/query_schedule_fixture.bin | Bin 0 -> 296 bytes .../fixtures/poseidon/right_g1_fixture.bin | Bin 0 -> 160 bytes .../right_msm_inputs_digest_fixture.bin | Bin 0 -> 40 bytes .../poseidon/right_msm_terms_fixture.bin | Bin 0 -> 8334 bytes .../fixtures/poseidon/rust_trace.json | 1047 ++++++++++++++++ .../fixtures/poseidon/solidity_trace.json | 1 + .../fixtures/poseidon/verifier_trace.bin | Bin 0 -> 22515 bytes .../fixtures/poseidon/vk.bin | Bin 0 -> 4128 bytes .../fixtures/rsa_signature/instance.bin | Bin 0 -> 712 bytes .../fixtures/rsa_signature/proof.bin | Bin 0 -> 4080 bytes .../fixtures/rsa_signature/vk.bin | Bin 0 -> 4381 bytes proofs/solidity-verifier/src/trace_replay.rs | 296 +++++ proofs/src/transcript/implementors.rs | 36 +- zk_stdlib/Cargo.toml | 3 +- zk_stdlib/examples/poseidon.rs | 6 +- 35 files changed, 2847 insertions(+), 5 deletions(-) create mode 100644 aggregation/tests/ivc_keccak_final.rs create mode 100644 proofs/solidity-verifier/.gitignore create mode 100644 proofs/solidity-verifier/fixtures/ivc/instance.bin create mode 100644 proofs/solidity-verifier/fixtures/ivc/proof.bin create mode 100644 proofs/solidity-verifier/fixtures/ivc/rust_trace.json create mode 100644 proofs/solidity-verifier/fixtures/ivc/solidity_trace.json create mode 100644 proofs/solidity-verifier/fixtures/ivc/vk.bin create mode 100644 proofs/solidity-verifier/fixtures/poseidon/adversarial_fixtures.bin create mode 100644 proofs/solidity-verifier/fixtures/poseidon/algebra_fixtures.bin create mode 100644 proofs/solidity-verifier/fixtures/poseidon/decomp_pairs.bin create mode 100644 proofs/solidity-verifier/fixtures/poseidon/evals_signature_fixture.bin create mode 100644 proofs/solidity-verifier/fixtures/poseidon/gate_eval_fixture.bin create mode 100644 proofs/solidity-verifier/fixtures/poseidon/identity_g1_compressed.bin create mode 100644 proofs/solidity-verifier/fixtures/poseidon/instance.be create mode 100644 proofs/solidity-verifier/fixtures/poseidon/lagrange_aux_fixture.bin create mode 100644 proofs/solidity-verifier/fixtures/poseidon/pairing_fixture.bin create mode 100644 proofs/solidity-verifier/fixtures/poseidon/proof.bin create mode 100644 proofs/solidity-verifier/fixtures/poseidon/query_schedule_fixture.bin create mode 100644 proofs/solidity-verifier/fixtures/poseidon/right_g1_fixture.bin create mode 100644 proofs/solidity-verifier/fixtures/poseidon/right_msm_inputs_digest_fixture.bin create mode 100644 proofs/solidity-verifier/fixtures/poseidon/right_msm_terms_fixture.bin create mode 100644 proofs/solidity-verifier/fixtures/poseidon/rust_trace.json create mode 100644 proofs/solidity-verifier/fixtures/poseidon/solidity_trace.json create mode 100644 proofs/solidity-verifier/fixtures/poseidon/verifier_trace.bin create mode 100644 proofs/solidity-verifier/fixtures/poseidon/vk.bin create mode 100644 proofs/solidity-verifier/fixtures/rsa_signature/instance.bin create mode 100644 proofs/solidity-verifier/fixtures/rsa_signature/proof.bin create mode 100644 proofs/solidity-verifier/fixtures/rsa_signature/vk.bin create mode 100644 proofs/solidity-verifier/src/trace_replay.rs diff --git a/aggregation/Cargo.toml b/aggregation/Cargo.toml index 3f98fa3a0..8e4eaae38 100644 --- a/aggregation/Cargo.toml +++ b/aggregation/Cargo.toml @@ -28,6 +28,7 @@ midnight-zk-stdlib = { path = "../zk_stdlib", version = "1.0.0" } rand = { workspace = true } blake2b_simd = { workspace = true } sha2 = { workspace = true } +sha3 = { workspace = true, optional = true } [dev-dependencies] rand_chacha = { workspace = true } @@ -54,3 +55,12 @@ fewer-point-sets = [ "midnight-proofs/fewer-point-sets", "midnight-circuits/fewer-point-sets", ] +# Enables `IvcProver::prove_final_step` / `IvcVerifier::verify_final`, +# which produce / consume the LAST IVC chain proof under a Keccak +# transcript instead of Poseidon. Used when the final proof needs to +# be verified by an EVM Solidity contract (Solidity Keccak is cheap; +# Poseidon is not). +keccak-transcript = [ + "dep:sha3", + "midnight-proofs/keccak-transcript", +] diff --git a/aggregation/src/ivc/prover.rs b/aggregation/src/ivc/prover.rs index 03d0b388e..9dfe3eeb4 100644 --- a/aggregation/src/ivc/prover.rs +++ b/aggregation/src/ivc/prover.rs @@ -154,4 +154,111 @@ impl IvcProver { acc: self.acc.clone(), } } + + /// Like [`prove_step`](Self::prove_step), but emits the final-step proof + /// under a Keccak-256 transcript instead of the Poseidon transcript used + /// by the regular IVC chain. + /// + /// This is intended to be called as the **last** step in an IVC chain when + /// the resulting proof must be verified by an EVM Solidity contract: the + /// EVM has cheap native Keccak (the `KECCAK256` opcode + `keccakf` is + /// ~36 gas/word), but Poseidon over Fr requires hundreds of EVM ops per + /// permutation. + /// + /// Compared to [`prove_step`](Self::prove_step): + /// - The off-circuit verification of the *previous* proof still uses + /// `PoseidonState` (since prior steps emitted Poseidon-transcript + /// proofs). + /// - The IVC circuit's *in-circuit* re-verification of the previous proof + /// also still uses Poseidon (the IVC gadget hard-codes + /// `PoseidonState`); this is what makes prior-proof verification cheap + /// in-circuit. + /// - Only the **outer** Fiat-Shamir transcript used to produce the new + /// final proof switches to Keccak. + /// + /// As a consequence, a proof produced by `prove_final_step` cannot be + /// folded into a subsequent IVC step: the next IVC circuit would expect + /// a Poseidon transcript. Use this only as a one-shot terminator. + /// + /// The instance shape is identical to [`prove_step`](Self::prove_step)'s + /// output and can still be queried via [`instance`](Self::instance). + #[cfg(feature = "keccak-transcript")] + pub fn prove_final_step( + &mut self, + transition_witness: T::Witness, + ) -> Result, IvcError> { + use sha3::Keccak256; + + let next_state = + T::transition(self.relation.ctx(), &self.state, transition_witness.clone()); + let is_genesis = self.proof.is_empty(); + + let vk = self.pk.pk().get_vk(); + let vk_repr = vk.transcript_repr(); + + let fixed_bases = midnight_circuits::verifier::fixed_bases::("self_vk", vk); + + // Off-circuit verification of the previous proof still uses Poseidon + // because prior steps were generated under PoseidonState. The + // resulting `proof_acc` is what we accumulate and what the IVC + // circuit re-verifies in-circuit. + let proof_acc = if is_genesis { + Accumulator::::trivial(&fixed_bases.keys().cloned().collect::>()) + } else { + let prev_pi = [ + AssignedVk::::as_public_input(vk), + T::format_public_input(&self.state), + AssignedAccumulator::::as_public_input(&self.acc), + ] + .concat(); + + let mut transcript = + CircuitTranscript::>::init_from_bytes(&self.proof); + let dual_msm = plonk::prepare::< + F, + KZGCommitmentScheme, + CircuitTranscript>, + >(vk, &[&[C::identity()]], &[&[&prev_pi]], &mut transcript)?; + + if !dual_msm.clone().check(&self.params.verifier_params()) { + return Err(IvcError::InvalidProof); + } + + Accumulator::from_dual_msm(dual_msm, "self_vk", &fixed_bases) + }; + + let mut next_acc = Accumulator::accumulate(&[proof_acc, self.acc.clone()]); + next_acc.collapse(); + + let instance = IvcInstance { + vk_repr, + state: next_state.clone(), + acc: next_acc.clone(), + }; + + let witness = IvcWitness { + prev_state: self.state.clone(), + prev_acc: self.acc.clone(), + prev_proof: self.proof.clone(), + transition_witness, + }; + + // The ONLY difference from `prove_step`: emit the new proof under a + // Keccak transcript. The IVC circuit's gates are unchanged - the + // outer Fiat-Shamir transcript is the only thing that switches. + let proof = midnight_zk_stdlib::prove::, Keccak256>( + &self.params, + &self.pk, + &self.relation, + &instance, + witness, + OsRng, + )?; + + self.state = next_state; + self.proof = proof.clone(); + self.acc = next_acc; + + Ok(proof) + } } diff --git a/aggregation/src/ivc/verifier.rs b/aggregation/src/ivc/verifier.rs index 6309e371c..b6eaf856f 100644 --- a/aggregation/src/ivc/verifier.rs +++ b/aggregation/src/ivc/verifier.rs @@ -28,6 +28,16 @@ pub struct IvcVerifier { } impl IvcVerifier { + /// Returns a reference to the canonical IVC verifying key. + /// + /// Useful when an external consumer (e.g. an EVM Solidity-verifier + /// renderer) needs the underlying `MidnightVK` to drive its codegen + /// or to read circuit metadata (constraint system, fixed + /// commitments, permutation commitments). + pub fn vk(&self) -> &MidnightVK { + &self.vk + } + /// Verifies an IVC proof against the given instance. /// /// Checks that the proof is valid with respect to the given instance by: @@ -85,4 +95,58 @@ impl IvcVerifier { Ok(()) } + + /// Verifies an IVC proof that was produced by + /// [`IvcProver::prove_final_step`](super::IvcProver::prove_final_step) + /// (i.e. under a Keccak-256 transcript) against the given instance. + /// + /// Identical to [`verify`](Self::verify) except that the outer + /// Fiat-Shamir transcript is parsed as Keccak rather than Poseidon. All + /// other checks (vk match, decider, accumulator pairing) are unchanged. + /// + /// This is the off-circuit twin of the on-chain Solidity verifier, useful + /// as a sanity check before deploying / dispatching a final IVC proof to + /// an EVM contract. + #[cfg(feature = "keccak-transcript")] + pub fn verify_final( + &self, + ctx: &T::Context, + instance: &IvcInstance, + proof: &[u8], + ) -> Result<(), IvcError> { + use sha3::Keccak256; + + if instance.vk_repr != self.vk.vk().transcript_repr() { + return Err(IvcError::VkMismatch); + } + + if !T::decider(ctx, &instance.state) { + return Err(IvcError::DeciderFailed); + } + + let fixed_bases = midnight_circuits::verifier::fixed_bases::("self_vk", self.vk.vk()); + + let pi = + IvcCircuit::::format_instance(instance).map_err(|_| IvcError::InvalidInstance)?; + + let mut transcript = CircuitTranscript::::init_from_bytes(proof); + let dual_msm = plonk::prepare::, CircuitTranscript>( + self.vk.vk(), + &[&[C::identity()]], + &[&[&pi]], + &mut transcript, + ) + .map_err(|_| IvcError::InvalidProof)?; + + transcript.assert_empty().map_err(|_| IvcError::TranscriptNotEmpty)?; + + let proof_acc = Accumulator::from_dual_msm(dual_msm, "self_vk", &fixed_bases); + + let final_acc = Accumulator::::accumulate(&[proof_acc, instance.acc.clone()]); + if !final_acc.check(&self.params_verifier, &fixed_bases) { + return Err(IvcError::InvalidProof); + }; + + Ok(()) + } } diff --git a/aggregation/tests/ivc_keccak_final.rs b/aggregation/tests/ivc_keccak_final.rs new file mode 100644 index 000000000..dffe2a0cf --- /dev/null +++ b/aggregation/tests/ivc_keccak_final.rs @@ -0,0 +1,209 @@ +//! Phase 2 unit test: IVC final-step Keccak round-trip. +//! +//! Builds a 1-step IVC chain over a trivial Poseidon-hash transition +//! (minimum overhead: 1 Poseidon iter per step), proves the final step +//! under a Keccak-256 transcript via `IvcProver::prove_final_step`, +//! and verifies the resulting proof off-circuit via +//! `IvcVerifier::verify_final`. +//! +//! Gated `#[ignore]` because IVC at the smallest viable k (~18) still +//! takes several minutes to prove on a typical laptop. Requires the +//! Filecoin SRS at $SRS_DIR/bls_filecoin_2p (defaults to +//! `./examples/assets`, which is checked into the aggregation crate). +//! +//! Run with: +//! cargo test --test ivc_keccak_final \ +//! --features keccak-transcript,truncated-challenges \ +//! -- --ignored --nocapture + +#![cfg(feature = "keccak-transcript")] + +use ff::Field; +use midnight_aggregation::ivc::{self, IvcCircuit, IvcContext, IvcIO, IvcState, IvcTransition}; +use midnight_circuits::{ + hash::poseidon::PoseidonChip, + instructions::{hash::HashCPU, *}, + types::{AssignedBit, AssignedNative}, + verifier::{BlstrsEmulation, SelfEmulation}, +}; +use midnight_proofs::{ + circuit::{Layouter, Value}, + plonk::Error, +}; +use midnight_zk_stdlib::{ + utils::plonk_api::{load_srs, SrsSource}, + ZkStdLib, ZkStdLibArch, +}; + +type S = BlstrsEmulation; +type F = ::F; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct State { + cnt: F, + val: F, +} + +#[derive(Clone, Debug)] +struct AssignedState { + cnt: AssignedNative, + val: AssignedNative, +} + +/// Trivial IVC transition: hash `val` with Poseidon once and bump `cnt`. +#[derive(Clone, Debug)] +struct PoseidonChain { + std_lib: ZkStdLib, +} + +impl IvcContext for PoseidonChain { + type Context = (); + fn new(std_lib: ZkStdLib, _ctx: &()) -> Self { + PoseidonChain { std_lib } + } + fn write_context(_ctx: &(), _writer: &mut W) -> std::io::Result<()> { + Ok(()) + } + fn read_context(_reader: &mut R) -> std::io::Result<()> { + Ok(()) + } +} + +impl IvcState for PoseidonChain { + type State = State; + type AssignedState = AssignedState; + + fn genesis(_ctx: &()) -> Self::State { + State { + cnt: F::ZERO, + val: F::ZERO, + } + } + + fn is_genesis( + &self, + layouter: &mut impl Layouter, + state: &Self::AssignedState, + ) -> Result, Error> { + let cnt_is_zero = self.std_lib.bls12_381_scalar().is_zero(layouter, &state.cnt)?; + let val_is_zero = self.std_lib.bls12_381_scalar().is_zero(layouter, &state.val)?; + self.std_lib.and(layouter, &[cnt_is_zero, val_is_zero]) + } + + fn decider(_ctx: &Self::Context, _state: &Self::State) -> bool { + true + } +} + +impl IvcIO for PoseidonChain { + fn assign( + &self, + layouter: &mut impl Layouter, + value: Value, + ) -> Result { + let scalar_chip = self.std_lib.bls12_381_scalar(); + Ok(AssignedState { + cnt: scalar_chip.assign(layouter, value.as_ref().map(|s| s.cnt))?, + val: scalar_chip.assign(layouter, value.as_ref().map(|s| s.val))?, + }) + } + + fn constrain_as_public_input( + &self, + layouter: &mut impl Layouter, + state: &AssignedState, + ) -> Result<(), Error> { + let scalar_chip = self.std_lib.bls12_381_scalar(); + scalar_chip.constrain_as_public_input(layouter, &state.cnt)?; + scalar_chip.constrain_as_public_input(layouter, &state.val) + } + + fn as_public_input( + &self, + _layouter: &mut impl Layouter, + state: &AssignedState, + ) -> Result>, Error> { + Ok(vec![state.cnt.clone(), state.val.clone()]) + } + + fn format_public_input(state: &State) -> Vec { + vec![state.cnt, state.val] + } +} + +impl IvcTransition for PoseidonChain { + type Witness = (); + + fn arch() -> ZkStdLibArch { + ZkStdLibArch { + poseidon: true, + nr_pow2range_cols: 4, + ..ZkStdLibArch::default() + } + } + + fn transition(_ctx: &(), state: &Self::State, _witness: Self::Witness) -> Self::State { + State { + cnt: state.cnt + F::ONE, + val: as HashCPU>::hash(&[state.val]), + } + } + + fn circuit_transition( + &self, + layouter: &mut impl Layouter, + state: &Self::AssignedState, + _witness: Value, + ) -> Result { + let scalar_chip = self.std_lib.bls12_381_scalar(); + let val = self.std_lib.poseidon(layouter, std::slice::from_ref(&state.val))?; + let cnt = scalar_chip.add_constant(layouter, &state.cnt, F::ONE)?; + Ok(AssignedState { cnt, val }) + } +} + +/// Drives a 1-step IVC chain whose final proof uses a Keccak transcript, +/// then off-circuit-verifies the final proof via `verify_final`. +#[test] +#[ignore = "slow IVC proving (~minutes); run with --ignored --features keccak-transcript,truncated-challenges"] +fn ivc_keccak_final_round_trip() { + use std::time::Instant; + const K: u32 = 18; + + let srs = load_srs(SrsSource::Filecoin, K, IvcCircuit::::cs_degree()); + + let setup_t = Instant::now(); + let (mut prover, verifier) = ivc::setup::(srs, K, ()); + println!("[ivc-keccak] setup completed in {:.2?}", setup_t.elapsed()); + + let prove_t = Instant::now(); + let proof = prover + .prove_final_step(()) + .expect("prove_final_step should succeed at genesis"); + println!( + "[ivc-keccak] prove_final_step completed in {:.2?} ({} bytes)", + prove_t.elapsed(), + proof.len() + ); + + let instance = prover.instance(); + + // Native verify_final must accept the prove_final_step output. + let verify_t = Instant::now(); + verifier + .verify_final::(&(), &instance, &proof) + .expect("verify_final should accept a prove_final_step output"); + println!("[ivc-keccak] verify_final completed in {:.2?}", verify_t.elapsed()); + + // Cross-check: regular verify (Poseidon transcript) must REJECT a + // Keccak-transcript proof. This guards against accidental fall-back + // to the wrong transcript. + let crossed = verifier.verify::(&(), &instance, &proof); + assert!( + crossed.is_err(), + "Poseidon-transcript verify must NOT accept a Keccak-transcript proof" + ); + println!("[ivc-keccak] cross-transcript rejection: OK"); + + println!("[ivc-keccak] PASS"); +} diff --git a/proofs/solidity-verifier/.gitignore b/proofs/solidity-verifier/.gitignore new file mode 100644 index 000000000..f49938ec9 --- /dev/null +++ b/proofs/solidity-verifier/.gitignore @@ -0,0 +1,2 @@ +/cache/ +/out/ diff --git a/proofs/solidity-verifier/fixtures/ivc/instance.bin b/proofs/solidity-verifier/fixtures/ivc/instance.bin new file mode 100644 index 0000000000000000000000000000000000000000..9247d55a50fd0ab415307885a5f1bc9985cc2e75 GIT binary patch literal 3528 zcmZ{lbx<4F7Kb57&=hwHK?}v9P^1)>qQy3Na1ByiOR)_StU!U{?ph#=wrJ5(Tv8;s zLver8-FY*8GyBdT=lnN<82-8iHVj12Bb~0~G{Hf; z!c9rNr$1@|!&@yXJeUXnJo8T{96lk5a=uA1J_fr?2R?+@60j8gB5#a^lkQ6I-qkt% zQM{IKFO}dc3^94R+becBKK;8-@qz9D{YH;?MD|Y^cKa?N=I@twzw>{|n7?L{B*EjA zfNl!x7SSG2--yq5dV5`uk5)sHXnd}IcW^gehL|TrWBGp?*1$`grw@$9rToR?t-^Fq zjT=>tF_^ZX8K8)2T_{5(`c=4wwH4@hAJYh1+2$&{(_}EIh*X&Id`(tp5%?XlD^{3$^=V(Xu98Hpbe_VDhm&5++o-Lc_Y{b41koGRL+PyS|B0aEL) zn&Ix3nLqnUXdVsNC&EaYu6mZQN#u79^mR;Mk*KDEtrey{40^}Yvwb6o6j64ypXcCr zl;<>wJF0I@vj-H25Al*dmS}vw9*6lXaIO!4T3 z42#(Z&l#b~UM@#GAgXuHxRa}Q@8bQla4f<-OhO;C%@Lf6w!`29liK}a1USnC;(L=N z#k{SK)4AZ_kHd4A&vHz&_Ojd#U%MUKo;uZ5u;4JoFi)~NCQ^raun$!O8E>wg7}VA| z&z?0pyH^EBaOZK2#u%p=Ha#PTs2hqL{$y>pY|Kva z>tSRG(7`n#jAnK6UOU%pC`Yu~yZe5aBr(eB^;J*RT=&Wx|3m!*GuxuUE#|bHY|2A< z6)ak_z09}g2wx4Xfse(yFK(t&g0j<1R0BMZLE@jsH*V==nax4Tz5e)`Aq4c}30`oB z{a@IT&ak@ghrYbqO0B}KxM3a?=W>mu5KKQgGZ(z8+y)O|Zw4WZ4X{;98rdg-EBxMf zi2T4f><(4#+&W7ECi4)BpfU)dlYyxGB^2zX^h&>DbWzly38k?;)qQZQEM8EtH830- zOcH>+S$d4tp*yXGDWe>)+qZuz?ot!mD@EJaN-9dZF>)CxSN25bmyckXyraie3mD_5 zI1OAV76$9A>(iLY)tpma3TgUU7$yF9^qfx z`xnNSl92u4uv#-x_&`f+ZY@Go09X*ZU>2*%7{A0~v){1HkjWqPYo~G!&1ma~uexY$ z>u%V>HCd1|dPEu*R~c``G+qB^=UOHKdTaLarH-9$yQF!+D{xi%1;0p$vWG)`R-}%4fzyF`1an5W7 zHnL6&9b_I&oO$0X9HEJp8&V|w&`k614(TNk55LP)x#PWS3k=ecR42U({ly#jRj?t+ z!@;n!&U>d|Wi%@$JGh{?Km`MiJNAqJx5#-GHi2ir7aYBb!)e_x)T;J*3%j@JN0Ztm|Catn-%Vm@u8wdHHnuaZVw$vmS7mIrE)lEQl*fRK#sYv%j$W z+%Jxnxrjytf4Vve~aVFg#XK&tQ7=aL4AkG5sPfh;Wru;E4Qf7rpM#o&)4n*%)QLb5 z2k0{C6UDfwa^;2i$1*W@f7a?u+u+H~092LZ(VEp@BH!RYxAdS%6;f}_^5n!QPig?2 z>X%yZV6?m%XljE{p#_(Bidb46X z#8PVhF+ioq_Qot&ig0&$j+J>ygLh!5g?i?-Q3k^c|8FC;h{oJJ>6E&R^QAc3np@@4 zmzZ_hMIBtulTC|yS01;$$fR<;l1CiK^BSw=G<>|TX zUWYY1`GW{so}THLt1ao!URg(??QwpciyGDvU)r|5*Zfv%WD}qWs~8orHL6#ASfSU_mzq5PnRKYMp&2mF*)>QeL&* zM^#FBgWVSrKS={{(wFgjEav_aC|ciEFkm@*O45^Rc|z)s3lAIk`4#DzVz4DYI}Q1wOhe zeXW%_UrQnq9{mA_Tt5{mc03{IJQ2heb>TiHglLz3flgb7#Xi^D&0#$!3?uETo76*a zb4KJ08aXo%X#^Q|Nv_{8`1W9h(q|DuW+eJ7=u8HC2&5bKmt;bL<^~mNP>K$gGqHsu zy&?{pKo1mcGD%MHngz%c&X^;wpm9_q3%3@IT;>$sFhrRIQDz%WHTHIv?didrsW=jK zx!1)o1LKx$f(3vCe?lqAplbc&K1~cGb!Q|G=6-o`Pi|~dW0T`ln&&1fW39om^=`0S zQ4JCATk&dTvyfc$I$iPCMv!60P6>>o0LHdh1TesL3s1QC043zlG3J+B`c^@`FZ)ey zazhC;B*W98=Ed2^b@5c7qmn;gDs-qIm^(^=qCfhqg3kytiv;>%2O;w;U{}L#UQSzT zv{QD0;5f@3D49H@+uk^7lg#gp7%Gz!6Gg~`Auhuu->mNav7BeDBinTT9k}?Mmc zXH}@R5{4ukL+TrfMs;(_^{;Wsvvu-F3z%@8_bt4n?wd#T(6ix`2+-Q`T}sVwMJx0e z(^4u7c9aoO_*0cCbta$LrR0zdvl=_rQ};O8f|Cc#HYl2S55x6>ShGy3p3$*=;b-R7 z;Egv}6emlR;|Hw`#kII(=K|VJfOfY^)&ip3Q`ru!gr@#O8mC)`$dpf7GP$VCjO{*j Qv#xewZ=xlnO_ciXKNgEPzyJUM literal 0 HcmV?d00001 diff --git a/proofs/solidity-verifier/fixtures/ivc/proof.bin b/proofs/solidity-verifier/fixtures/ivc/proof.bin new file mode 100644 index 0000000000000000000000000000000000000000..910bd9d65092fc823312c99eca1732f391ffb29a GIT binary patch literal 9824 zcmV-mCZE~0qwN{AXqIhJdO}A0Ej|D5_LI-9CJwx#!Ub@!J&?O+fsY?4{9f%RZP_u< zsP{yHa$Qe?Yt-Sg)Jb!v%KD@4Xk%W1w^o>L9)6r!sEzJh6E0SD#XPIQrezIJcOsl|30qDSbdPK9b* z;f}pJ!y%~YCpJ_O5jrfC#p4(?8tO6Pl6#qs*Q#mvtCZ@`>?N9zlut^TC+&kt!0DkN ztJeyYvc4AB0o!<_j=vj@gDeud*0DpKBuSz&`*<|T?tm(s7rB~;@>=^$R-L0)GmKh| zZ*Py9aMSKgOm-10nUy2`lE)Irj*gl899V!xPSFLf#a+Mq6BoT04Jpi?QD@PF=n|KLs_Ibu%Ql8sK6W!L{fJo3j`>MmA+~ zJ&(S6D$+&e6FhfI-*I3;bsN-%BU&9Gct`PtrbPk~VT;;v;J6(R9co zkj<1>@;FPW)3y4xWL?dsmZ@8m3(7H=0taI}5MD(#@xj@|f&r7TOVkQO{Y4IJU_&ZGbnRD=s7* z(WCZ?_`H^i$|_F@C(fUQr6m6Xq!daT;Qe^9y0yzXbMf^>0Q%R>Sm%J8W5c z`O$-ePRq2qD5pW?r);1n;Z4l70grsOC%}8ouABR-H9rPNAh6$XQD60jk3?yX(y(M$&fWfM(eL#@DAJ9Fu05S<(KdG^Kz=A~Pp@Kn7ad zUol0;0r+s^_@I3B0jvbdSBKlWBSN+l{*iKb%Zb&>)EZtPyRRsfR zYl9co*`Mx1_mhz68(zIIJn+W9GVp4x+EJ;k1}G}(_|fv!=jZ?1!|0y}NZW@2SI;Y> z-hflrF~S4@9Drk^wBe2kfvvERYvaUj1evB^>%3BI3h~s(@YXP|A9nu*^V)|>->Hv- z*s0J+sY?dD&2jPAHx44HW6-F14D1@p)ECut@I4roJD1S7X`kMK2SjnJU-(OFBsJvT zHHJp=_{;6m2Z6bbRw|{87wc+U>elfKV#<@<<|r4}vB=YXVC%b%|DjuIR2N-=^rzh>jFodLkdhv=^KFdaca;7cnWZt|`KgkBt2bXy5@tSt_dH zX?9|)laGLP1bXZrqI*h}jj~jsQ-HdY?31ydT==NI9YA>JOB)&raM=g-&SVG{{~Z;r zaHRc^aLcx$-I_2YhT;{E%V*vp>sij|U^ABJY4c2d!|Pk$oI~=_Xf?(b>PkaOC>zj-zmbe}yPNwId&|D2{5nJ@~9i(lQGT zEsh(%mSvJImMiiEad^4`i`83fWR%69ZdKcW&hQD{F3RH@r-Va9uvR(}gd_3T+c6*} zG^|sj^T+Nz*VQ12Tki>;XZWTs@NkM_i)$VhQAt1G0*Gx5S=dCZ3q@>|qLNe!ZG|u> z8BEbve&;bA@=cDqjo;}=L1a5Gu0mshSQ{X!7`oDOCYLjyh+j%9Wp;LEppD^aD}+zG?Ct(spC*=2Xod)M7oDN8vfE9} zDsFQ-r>O=yUNdM#DDXibS0^YiXMpp4?=b;bow8&DU0@40y>vbuafm4BcPjL<*OQPc zck2*tEEs@s0RJpn!?C_hLrq7{x&dkFwQ>;GTDsBwya%QON|stEh`0_>zcX%i+|{nG z@DOb!V!8-aCane$rBwe2wQ%6IRUaK@3lqF>-1G0K-T*=aoVtdEcD4%}ksOC-{{d$U zv_l9MyA%zQ^YAh|9vZAj-{F;v`tnT`#2Z({*LC}TYM(#5DznyUgBP}MvIy+ZMOiLF ztY!qsb%L;F*$eiWAtV|1Y?J$v=XY#f5NA!i7?Q6;XelIQs+eYuEab#?rRZBXCWohY zzKPdc(U75a$N7_UX7P8JJxa@rXmEuvE5lT7S%eQjLU{WD+N1I5prN3gQOqy%(jbNOAO;+LRT8_R+eRxlgX40bWYoF^fKsq&px7_Kb`Y!UIGr&ucl<*U z7G0p)K0>0!vAX*iLX&FFUy>|ZLAZMFT!nv7QSif5w}nszI_Xp_F;h2y`HwgouQEc! zx#tqRh&4d@OuMYK2>-Wds*Jtu5Uyl5RVMLIb+k!ITP9Qu4Nay@SGy|6$*b+o*v44I z$aRa_>s3JRy`*BAJ;zgl3)V9p($fYjb1s*xR|w3nlj&%2NHzV<#n4km5vNB!&{hJ~ z(i|dqOt~*qBx<4!<{pTdZ(R6w-9MKauWOLU~idJUhc*WE#3ezb5 zGjCaRWPV`Rkv%AQg;dA>BCRnh$!l@0li$B;~ZUS|kW@0v-K70I8 z&nJ_4Ta#hiv^y)uQ1F(4*(8x?NLeS`v~_%kZD|@*V0QbdMa4w&z|`rH>VQ)(C^_<} zmShQeLrFhJ4$R|UQ13-oBcUgAbxT`p$8 zmY)C|{^{VioFN?tJl}*_&MOzG%sw7cu>@tvuXoR;i9l`r8<3Hf9)}&lFh5C7wGBt3 zI>Yg)C!UcK>e6RB{8xoLPk?*5l*3x?qVnTAYH3zwch}$`#o-b8O_9LSt(3lEFM)Dn z3)x0_UX7aJqJ|Ap#SiIq>1SqIHtKabdTCtem5%$Zml;MIQ4w?fov_cAdl{JKh*?CQ zY^b85YR9X)0>ah9nVSKC&nAXP8K`l7R-GH-rc^-g<5U~+cB0%}yy)@+L9G;K^u0$1 z;jTb4t1e0;*zCbzb|jNW!?9*m7W5_=`)ymv&!*%ZVK`APT%!|7W2tIg@DN>lxA)!0 z(ta`}@)RU39&eRX2q}6Da*7=mNZ_HniuBx!dMx)8?Hv5^(za??W+uJ^H%(apBM$-9 zxsERDn3S0~5B9noPQBeG2fS?@T{$G@3BpZ{)&Wg4@RK?xVI?TfwP7A3KR8jQYjQW7 zSq-13yan=b+6Z{T@ew#SqgM}d;WVTCRP=viwlrS)A*|C5lWyb^ ztb&W3(Du?Ly2PKe0U|!xY>6EoHx@6E7r!B8R=-(XJTSfRA9Btc@;sySG$x*z0k1RTC{x)y%Hdnv5DX{0kt zqZIO<-3}agto1kGa-Ns-S3}vzldtwr;4G_<#v9T?#P$rCjgL1L%U)#;HSnbpX)QP! zr$NUoV_!cw8;=CR0G*)9ckZGp)0cXAea|QY3`gw(0b~x-C z7C$!&D0#|KC7v%*uH851RXP7s^yQfTPB|Pi*n3Y((!mXETox(wf?nbO@q+jeSxap2 zyj%Clm`BV+uu*WXUu;pK;(oQa))%jQE+qQWSb!sboWVZ8F#N@oUZ)j3gXBF(uN1@4 zONts2J+i-%#2k`ac&09%PRnN0$4#w+)Epq^?~Jyx0n(Ub;?Qpt^1dOXukoBgwk9j; z%YOb0V(=Im{69eoz3CL!Tw)$1fE|O{V%fB7KvI-L4;5$L87m52kmR8>O@PrZ-k;2M z2Z3aRvrCI;nk}zPL87IGv!WSnRsOYij)0h+ehe~{7A>fi;oK3AgP*&5gRe2zwJAp* zt=>YU(2>R|$licodSb9|bbFBsS%GBKVt}wI`;)1EmttN zD|mg0*2M`sSN~*lij4%V7b9&$uSLH1jO&Tk<%>HH@Snzufl%mm(u-@=$^R8H z!4zMI%B%?p8(f8<1rT^_5zI6oTkNhJl_j*f^qL9Q^u-8)0*?i=-zS9#$wupCUN6Qu zR_6SlyO>i?@(aO9A=(LR+K2FdwF2I#o^eOHnnabrtjz$BX82QLsXHY1Zcj3STMUBY zvi{u?IH#!7;$9NjBs{aHu}93=-G-;>X*^I6X&$}}e7etecD?$?d_E8yXIA#R|93uV zo_`s!szp1cc|djL4v_DR`*iTw~*I+rvvCr=^`P#Bb9)0YWb&#?z->?ebi^Lc{5xy zVt2_llMWuiHH$Jfc6cg!Ei%E;@iUjJli&VdT`v?Z)4?#8`pg-9->DB7*#CJ(mG8?T z;)gtta73#w(9|9Bv1AX9Ix$msx~~Z3jccZG6~vXd+bOt>ZUv5W{t*NF#Li3IJ+^)p z^Tmp&>EG@Wc~}`6LP@|^)>ckO=$2AJ7tJ;<|E%5^&MUmI)f9TaGcbOwxXJ}i->|%} zIwk}~apA`021~AJJ3~n_SF-l=+Oz)LxtWw&h=J8d)y*Wsy{vsEh;n_DRXGmOa)G9qhMf za3WXt=ASB=9y#-P-|+`Vt_>CtKb&u){8;hTpDPXo-Zn~eXvT!i*=aw=T`o!dlFLLh zJCS0^AO0KmomYwhotOVi`E%+gDx64N;6s%mN1+_7z-OX86G|XCY#fBj!MilyZIk*n zJv`9{8q87_I~EFq(@MKTS@*AgJzoPvQbDeH=+N(d9edccmU9-<^fn?YsZ5;IaVgEj zzTxsCz<~Bz9d1NgG_m#g5?@0C_JmFO%v=cp)Ie_QUL8I0IXOXuW9(ClQU{`vFZ*Mg z$>ca;ourbUm~F?e80WokIuO7LcICXxjOn%pbv?xxT@h+O=TFUsC{lkvOvRIyjs)lk z>fM2z8!>Y`Gxd_4Oy?1sNfcj>R~lc(Bor49qAx-bUAt~jiR>EYztTSCXJ<<#qZzgt zzv!tg!yHxog?00!LIqpaJkiGJnjtntX5TrxdmeAnD0VVMC|sJm`9{vK`%%=e;9d-x zofSqx=#vC+iKr@4)hWJ4uTnH{m_n4GIGGZ#;eTmn=VUyYtI`}Ji$3P-UFaPBxX%A~ zrJ6f1#<6)4%0(Xz`vy3}$r)B(Ua2($_<}L!5Z+xyzFM*ArFVUM`?CaNVRya4Sj9}t z|Js6Sh1c}d6$_m=hEd#Rz_*xp<&$+MFeh3TG?r*^q^quoJPf`%_jeo+WQ8K3(Z^-w z1o-Pwuf`6?l3O+W9%Bl82`QUT;a+1+)Rh5hM}YfAU(3!@{l1$A10P1G+V)ee2~${& zu(@Ba^pC7^`Jbk~htg{3fnV@*cho8*myJ=RMQu}yYWjW)kh75Bv)#4y3%9XZLPa8C zA0}5cNa+_}J5FNSjj7S^)V~5CnhWvp6ktEe^3B+$$$AAy(!?nEw5}q4G}Tz&ON|oz ze^^K6jxxUB>PCHAEnp_DSf6Y?U$7B!&=qw9WjfZMe;F6o$p;5)VI@AxD;qe!bfo8P za}ls>-wxQ{Vk@5PaA1==`+KF5cWWAs)}d_9mN4r~O+*{RF35vJGmiwd3K4?BQDjt+ zgNzD|B=15j?5pqy$(xy-CCdC{Y4P+DU`ifFGf zN~|lD5Y^x1J%Y7!W4c)qnNu9@KRmrb!Xs8EA}>J)mYM8hl#}rtn)WFUOG$V|qj?Wd zN0nr<^=SCs$97bHBGFn>EbVjUU+mp$7SNT|e5wSzebm%kF_Yc)EtCZqM? zwI^QDlm}c!dDi{r&T|Gi?A{u>crUhx=!&exY{;lU)R!#vPth#-k+*Vnq{0QNT}J2y z+1d{!Ej=~0#3g=RNzBgGg82Gc}dg7OaZ@SVs%^gUTHW&}hW z+O-is{s{7LGH8yY_{Sjiy78XXf`|ue@I9836d)qaep)YWQbN)*Cvq;&cbangVovMUn9LH3L}5F-q2O$A zY4p^MI@)4S+~b`m9u@=?cCRez0c#=Nur}h_S=246Jq?IR1tOqkN>eSN9QGU7z)xf< z-DewCD*~CGiJbC{aNv-D-|$peWMXWJ15|gN`@e2*mswUAb)fLx6Gkypb1^?&ko@Kd zR6{mQf+W%<^FwS_fxLb=6N}7WKl!l!kd|xeUVoQ^U58`MEw7!RO zKKv9XmI=MN2R?<4dDNTc`OzTm7Ls;SoKdy;%Yf6&OX*G$U zh2>gt8oEU=-|J$)qRk4nI3xAe!0{#<>6qkM=|uHVjzLa;)Cwqpj`S2Dz0DR=!q?`v z!=I!-u;*2pPanA4i1!_uYx8g8=KhY9M7}vtal_D z2H1QO$@YH`Hc=$)6sk;tr21>%VAnm+n^Mfezt_+rKMvcvH-uR3H+2K269%!l*6unG zYc(g{D-=Y8h(hTY>bt6J6R`I=&AkQ^Acj8j<&x@QwkCBGONQa0;GO2^ZA)ZqU`D47 zwKz!wWAHGSWo*uJFU;HT>UL`>XeoALrc9>$!^Wme7D6q0c_bu_^@`xQW-|z;o>3lEM!Q_nw z{ER4~n1KL>6!q6!>leLYpz>mZ1O>t$mlujS)jSzdf#cD-dZ>pDof0LdICQyPge!WyD8>JC>9N~3k#`*kW zb2&q&6U&xav>ky30QeQhs)h9o@RTcO%V;snXi|1K=bbg&+@jevpTD4lqsFrLKgv5k zT4)NiY51W6uyRlITyheP3dTyCAN)c%9sp z)MhYuFc%~e+SvS9)O!mHpt~9QF@Xt-`L(2Qp@?ZQ>Vw_n-oy|qB-^yPK@Bq`%n_WO z#<&05Uy0rgmhsf8M3LB2P0bNWsXde_C4@g=lASDy`eyWhqRz!m=V;=1HGI|)isD~T z1F}I8tNX2`Qo=*VSDsjDDveCI2cRxdXtw!{6-D92+nDLbxdRas4eSafHPEiIlcB>C zyv{g4=C!8MO8J6|;JnIl2qlM6)BP4rk9g9Z4$(R~yNhmv3?T4j!gI#w%P5A@FL_fn zhU-#93c3VLBr$UE9t^O^HQxN%Ht{&0`mKzXRG?A;l}iLkvh%|ZK7TX&(y_Ji9zJMz zt=Ca=#AltsK>=PE6&FTv@Gkk^$jE50X@hYa50BS;Ji$ji-TWS4BUg*)U}!qxp~7Qt zmOn(90cuQ??=oa?@!E1h)tYgS(Rc6r|B|{}xJa6QeS?P*e zUZPMt{K;c12oz~A^4H=fA4<}K%I+E6= z(QfG~F+BRD*wat@{bag=DUt+*1vD0=oazca*tf~_9AZ@`%o+d$oHq;qn}u&&aeZBn zzt`=`mEJKp{J%7G)R%wpGS}YRIoOZ|(qEz2J3bjapTVh0=A(_T)O>^gk4NVOO$#fJQ| z&I3~DD?lxF%GBZWbhj_A^y!ARhLsq$ysUs%>bve)Kg@q2j8D)oFSWEn!5~5u89$^O zDx{YxGx!9Nmues_D!Q%!tE9ntpl56>l5!vYZp}I9QoF>Jqn}-3v$G4X_yHXE*yjVL z1i1{cM}{}iSdV~FBnV|8yT+K^UzDJ6SXTXa~3 z{7Ot3U*p|XJOM6`KI>uy#m%`(eWfXTV$7_Dv|1$8I&1MZ)Xy=|x>wD)&)>=~u58vI zM`Upu$56cGWzqrHP*nEFERg_vR1)HKcypTY&IMC+FSX4NJ zUF4c?Gv7{nq}n^P)ju`la`p3HVeEMPom+0%ywFIa%g*Ya_KEq2i;6RWYnezGo@a!o zD{+Rev3u(mSF|M019q{oi&eL2Ec__|+Lj1f%ed?IP4Cd!-^p(Vex8LLgas2-KYU@5 zQ{Zw{w+|Ea>0(b-szPfu;Xmjw<=_Jq2gUP;qu}p%?Wd{_;K@mdbrfszg#TTR3Vx}0 z_!>Z{uTQ^IcGArg>py)Y_Re7QKFm6Lwi7u^+M~HAdSaU5-!hieaEfO{+O~2Q@icH&wB}0GSZ@RcfQS8~LswejM*O zP$5I@GRulh&tg~-co6%hky`8uFc@DZ5X-sAGWESwOrME=5g#0~jhJ)3;OuyQd&!+{ z;*I?+*h~)DEa9?ZLEzcHS$c9!F-I8xojebIDgjGZPJd8+P`*(z!cCF)N`DM$6<3KI$4rKq)-1(P>h60HE*u5AOw)^M*HRO|%qDXAV|%8~-ip5UVQJx8bQYtX7eSVNm1K_nrYo?Jxs`A!oW6OyeTRz&`IR^x8l7WT$f3IzE8 z&4O0HV#R9|Q!%THTB3%$owA(s%P4dh7c$5H=s}XapC?S0T&&L+RY@V^JnhEYQ=~r* zSazv9CYidub8L6OB4A*nmYxZ}1`22{_NXTPx;x=wDZsZ1T!yv{bs&bvg*+RwpYwJA zL>b8Y;izw1?B~-83jttDt72R;h=lb#--u4z@UyLf|M@Bu2g4p zjJrrpGbka;I2J~hVoBOQ6sRpvm7~VxS{#rXp{vrd;UiaMNBSg^4wz5{ixVagmi{6VA937H=8{3(ZbAMf+OhLN~%ju&wG zvRSu&qkr(j5gL85U~p&-xs7ME&kE5(kqh)kx&~GIat&5^s-=exAfBDDYwHlJ7=}*} zs$|Ap;}80D`a45u!ya-oQ5&Ae-E1VVxzsFpe56KC3iJgDq80sj928ueLpZU@4TS)0 zf}ReYkRGH!z);qGyGC;|(;9hISVB@7oy$c#w5rIf`X0 z`T1%Za;iwd_xCTu*C`5ON@aqINz?*8i%3vX zMG-_MtAsp@CJV^;5rF(#YtAGA!q^ykIgruq`y$K0X G&vsxZbJ7z4 literal 0 HcmV?d00001 diff --git a/proofs/solidity-verifier/fixtures/ivc/rust_trace.json b/proofs/solidity-verifier/fixtures/ivc/rust_trace.json new file mode 100644 index 000000000..3f5fe0189 --- /dev/null +++ b/proofs/solidity-verifier/fixtures/ivc/rust_trace.json @@ -0,0 +1,1068 @@ +{ + "entries": [ + { + "kind": "Intermediate", + "data": { + "tag": "vk_repr", + "fe_be_hex": "51a3ad301c5292dcc208abbe14bae00feebc636408a4b2c54d5edd07ffc492a2" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[0]", + "eip2537_hex": "9324a75f7803c2fb7be84f8f8f92397fc38f438472414438972d85137fa1e884f7fde28c1f4b1d3555b721915e60a4d7" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[1]", + "eip2537_hex": "afabdc807327c5de8fc13495028900d83ac62f2a9533b780582fdd18bd68e5e20287c861c82923e6576330099dec584b" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[2]", + "eip2537_hex": "ab042a4727ee1f333d73706db25bd1f229cb608765128af1b9377b3e2b4795e85503deb7f086fdb781a3c4d190205a40" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[3]", + "eip2537_hex": "986d652bf616055fcb845f79e496bb94a6dcdc7e332a53d2c1d2370a637b0d2a4f2636551e681cf1eb70c561defa62c7" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[4]", + "eip2537_hex": "a24481617df262dbd06b5fddf658f9c32e49c6403bff43252ea10fb35c37d987af93346ecee6db2b794f8ea6b2b60286" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[5]", + "eip2537_hex": "955ddbc47fea8bf6280024d6b455ebb60462d79a19f49187443bcdcec9b16a84f41f41367805521eb89f876c13cc2880" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[6]", + "eip2537_hex": "872ba4345eb8cc6bc2fca678eae0455ed17ceca164901368a453b3826fde369e8cf63db2a87b14fc54073a369ccabfe2" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[7]", + "eip2537_hex": "8e14088f20db449b8f49695c3c84d8d873faedfdde1ebbe87c39c81765d149380c313b4242278b752177fd7b661d0554" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[8]", + "eip2537_hex": "b5e8f709e287c8a92bcfae306d20a15034f8f04e0cd8dee49059feaec3e99e002d4268f36f613598b68b55cc3b6ffaf6" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[9]", + "eip2537_hex": "93c8f3366eb85800f928116e3a177bb36a5baa5b698e70afc838e2e9eac2b35a5a0895da21f21f86b4dd5833447118ac" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[10]", + "eip2537_hex": "a23d93bcefea6c36837e348911ea299e8f478c5097f803c74c9aa0752a09c812749102e3a3321193452d710a95ed97fa" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[11]", + "eip2537_hex": "8c509b6d23069f785cdcac3319040d8b5dd155bac5e4a3b815891e6a571dedf99a2d46b4670dfdaa7c89cf43fc4f7df9" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[12]", + "eip2537_hex": "b642bfcd07db3fb367d968a923c87f6c0db808f812cd9014a01f36d8b1a33b20de6409ebd7db009104cdf07ce9ddd872" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[13]", + "eip2537_hex": "8980d60b9cfe2b6d5327ced4400e8ac611d292991c0abd3a2c7e692ff63b866ebf4c1a54f15c38603e60ec9247578c24" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[14]", + "eip2537_hex": "b946984d70cd450a0f9def10b8f8694e58f74b5cfc521963170307df92baa7e8237fe25d9fe81c0421d909ad0f5a6b76" + } + }, + { + "kind": "Challenge", + "data": { + "name": "theta", + "fe_be_hex": "3ff9db99be64b81421248a42da80d4cd7ba4aa6bb94e8ca2dc7c04b5feaabccf" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "lookup_mult", + "eip2537_hex": "883447beb33d2cd632274e5745542ba9f827e3de62e2218a3139751cb8997abcbd90a1ca2dd9be5f6c861b06c2c9ebbc" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "lookup_mult", + "eip2537_hex": "a06266251c32ea2ac0f43fb5dcee056bb7e8002fd9ea8480c384085f1e788e542965d48ab07e31d0140987c4b566eca4" + } + }, + { + "kind": "Challenge", + "data": { + "name": "beta", + "fe_be_hex": "5e1004307780e019454167777748c63f1ceb0bfd8137c218f46a29d9fcd8997b" + } + }, + { + "kind": "Challenge", + "data": { + "name": "gamma", + "fe_be_hex": "0058d4bedb482ef6ec68cc4102d79d26a3e68c67a660554306766a08d0b4bae4" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "perm_product", + "eip2537_hex": "b992ef1d42c2f3db9b82a33465d87623c31772480224aceaa7bf46ab89cca003b1973ef7cb161a373cb00d968286aafd" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "perm_product", + "eip2537_hex": "b124921f1914ac76be4886f24155f31c4ec67e4bfd34b400eb9d1a282310d7c4e7577f80297f48c5e46e17247c9cc03c" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "perm_product", + "eip2537_hex": "9215d3b97960403604292c0e49b5560bda50f7de776039dd4792ddf1afcf4485e4036d4c6f3d84c6a730805f433432c8" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "perm_product", + "eip2537_hex": "89d2652251e8c6df970de2fc4b5090d192110de217fb3da3b11db3886c472d7b9d4f9b4eac8f599e90071394dc7efe0e" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "perm_product", + "eip2537_hex": "86302bed0abdb7528321e4170aaef2962c67c8926908dc990923971736f90f2173ea372b7110afe8d9e393cb73da0677" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "perm_product", + "eip2537_hex": "b36a8b2ee086077c3673c953aa45c68f947735789e2c386fe2c7bc768251bff3eea2ac6681d1ba9dbf24a774ffb41662" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "lookup_helper", + "eip2537_hex": "88451ae997148f4b0b042066e35fd50c3a39a24507945b0ea61d3a5557d39e45f01740cc16d469f688730cda358e55fa" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "lookup_acc", + "eip2537_hex": "b14d249dd6b8c44b4007503b116d26e684be9ff9d1e76c66b9a0182f4d4a5998bb0bbea3c807f2294543b0651afaaede" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "lookup_helper", + "eip2537_hex": "a2d3ee8eea5e7e755b69bb79d8fc18ca688ff7487afb894c3077ab6c68cb8d7151e96009cc5bea912ad2fcca23c63f26" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "lookup_acc", + "eip2537_hex": "84189193c82ea16ce40c6c3a7670c782d9ef22343531180d8f15aef0e3ac7714c6b4d2c3eb58485630260e95a094e50d" + } + }, + { + "kind": "Challenge", + "data": { + "name": "trash_challenge", + "fe_be_hex": "0de9348372df1d1e7432fcb24ef5ecbf1358ae5dbe88097c7df40bfcb8d7889d" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "trashcan", + "eip2537_hex": "a846009a66f994210618221e9d4c3b7dd2830290c5a316a1befb1cb3fa2fc5ef84b679e5e5766914673284cbf6a1f3f8" + } + }, + { + "kind": "Challenge", + "data": { + "name": "y", + "fe_be_hex": "717b58f00b4d3d2793757513329c2c1d50deb5b93956dfd2048548a4d7837d9f" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "quotient_limb", + "eip2537_hex": "b4a018c05d3d9fa4b36bf3f2ae2feca08a658412829a26fe41aec4134158fba7739358e125cea1a5914f72cf9b525e72" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "quotient_limb", + "eip2537_hex": "815a896114a233d654689dafd891428d126e7e620e30dee5a5f1c0d9ceabc0581fa23b37dbd0b7775aeabd9fccb1d3b1" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "quotient_limb", + "eip2537_hex": "b6b9678337e622f2d663b2293748d82b7102ac83213590b5813c5099481fa123f81cf582d5f7b4e81dd3ce024d79097d" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "quotient_limb", + "eip2537_hex": "a2c5f0060ffd5c4f8741b884ee6834a04366739fecb2bbef7f27b8e2d08065b4e979ede81b398a1ac6cad46664035ead" + } + }, + { + "kind": "Challenge", + "data": { + "name": "x", + "fe_be_hex": "25a95a70b527cd00441e66fc7716f297fce60d998bcc0158e816488a393e408c" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "committed_instance_eval", + "fe_be_hex": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[0]", + "fe_be_hex": "2aff2f04fce943e11f15b05b610d1006f7276184a3eb52764a9b162ef19b774b" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[1]", + "fe_be_hex": "613ce970681657d1997019c85dd9e468b607ebeb81c81bf5b3e373afcf25e417" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[2]", + "fe_be_hex": "5ad602afd794d4ba4fa04d59b442983c446b7a1924f36a1c869b6fc2718f1b33" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[3]", + "fe_be_hex": "1b19d3101f528b398c5636f9af64ceb203941f5a69906344f6018dcdd93ec9f8" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[4]", + "fe_be_hex": "63f62d35bef1451fe363e8ae48f478aa78d3775c1df49470ed9b08acc8a582f1" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[5]", + "fe_be_hex": "6120b59c0ef4b1422d808ba3bf08c4319da59fe04f3ed69e30cca0e30438fc02" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[6]", + "fe_be_hex": "04924d8799758ac570dbce5be1bccde9de9b44f64ba93506721bc8fd9b0bbaed" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[7]", + "fe_be_hex": "2cb4f7a76de56ef73523af97075a0498d7789853d23aad7fc12c7b7f7092a018" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[8]", + "fe_be_hex": "62b0bdacb806ca168215731c666c8d1b3d35241023674f4e8866119719dbfc3f" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[9]", + "fe_be_hex": "5d9ea3fb49590e6dbc8a42f662741fec9a7322411f948f8d3f54bd70191a6f3b" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[10]", + "fe_be_hex": "28c0c553847c9c7c586fa0c979a308b2d4c0c87e3c7061d200f7aef29776f159" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[11]", + "fe_be_hex": "48a364fdc95646a925fe21aaf84ed1fc70e97483af6ce14452c251c4fd20ab8c" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[12]", + "fe_be_hex": "4e4e184924e843a78524088430f840c2734476144dd5adf53bb35071bb7b2d0e" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[13]", + "fe_be_hex": "1061413a5fd1787a3f0d54dad842be30a6097a89c52db89360ecd2bbdc98dca9" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[14]", + "fe_be_hex": "64da1115a5c5107dc2aa9304d6dc5b3d83195eb097c28fc05cd57a218fbef7c9" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[15]", + "fe_be_hex": "33ffd290974cedb43eb4e5e4cf9ae0a8c6b1c45b45654b5e960ab34df74efb2a" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[16]", + "fe_be_hex": "5cfeb68e3f0b568eadbc559b3466c6d236161a3c8a040c0b4514b8145ce79238" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[17]", + "fe_be_hex": "18e676097fd27a394eecbfc4fc9e77b93bcec22fa31c8cb5df47148a24d61f5a" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[18]", + "fe_be_hex": "188899add7daab3c574a1ea69dc2462b98f4784965414b67fe968533a5046841" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[19]", + "fe_be_hex": "678d9102966f8980e4ab679b0e8d89062a8a4b4287350de27b073897526af3d3" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[20]", + "fe_be_hex": "2c69fff1c50460ad0cb18d96216a250fdca786373030ea394cf3e08493085c3b" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[21]", + "fe_be_hex": "69e0c4daf6c37e6c659ee977e0f911703782d74a535f30c757b45b83fc865da6" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[22]", + "fe_be_hex": "728344ed27b3ebaa17612c3ffb57a14efb120656258b47fa20c25ee225ef2171" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[23]", + "fe_be_hex": "4806c4286bf892869328dc209bc46314b2759765bdcbcb99744fa0f348545a75" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[24]", + "fe_be_hex": "0af784a7a802b57c08ffaea2b29653005cac85e3332f5cee2b754ab02b5e0bb3" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[25]", + "fe_be_hex": "15d0be507d1046854082c0ae8b1d4ff812a5b0f2899cccfb3d7877f355d5b83b" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[26]", + "fe_be_hex": "27f5830c86931649356219918d1fe8ed97eb4600aa3e46b4b173778d7671d50c" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[27]", + "fe_be_hex": "6e3f6903d3dc1e0e195f673938a9edfdd6dd456dcdcd560ad4d9dbfaf85f39cc" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[28]", + "fe_be_hex": "22ca4cfa850bf7487c8d7eb1cf2ec2c86a633768d9b9e2f16919b0830d3fb844" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[29]", + "fe_be_hex": "5a9e3590a4f9be93687d68104c9b6cb2cc9ed410fc07d6162dbc9f64decd7ce1" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[30]", + "fe_be_hex": "4b7d002e918dddcd4be4f0623d8f9c0a9533b24af17e5dc634df24e500729dba" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[31]", + "fe_be_hex": "5d3073c3a96253693c8df7ce25e52b38b8c78afd7950a60e21cda35c3e48b78b" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[32]", + "fe_be_hex": "2f1400afb1e213b69d45580bf9a0054f90e54c4dfaf340510358368d7eb037fc" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[33]", + "fe_be_hex": "54f6ff91933cf536ecfe439e01b2103b02aff564ad64cc03322803101fba3812" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[34]", + "fe_be_hex": "6bc15393109fc72220322b2e100a293d9b4d7f43828783f18f226701900cb91c" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[35]", + "fe_be_hex": "1071a9bbef3816d70ebf4b92b9755e176e2a8558a3bbb3d16ebe2570ffb34f1b" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[36]", + "fe_be_hex": "700c1d9c6e073b01b5c81986fb4df82a1ec87043972046372890ca3c7b5b0928" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[37]", + "fe_be_hex": "64a7cee5b982c3b7eecc70e2a7a96e53be8effb11d1040c9a73a9ebf334b1194" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[38]", + "fe_be_hex": "0175ebff5c51ed1bc0b0ac14b7193dafdf64b1c2cec0c23de2e781fa8394362e" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[39]", + "fe_be_hex": "6955a4c86380c065bd0c80036c3ab85fbdb52dcced432ecd68c48d9a1b274509" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[0]", + "fe_be_hex": "094662684cbadaf67132f94dfe2511cc8aee0f2519f2540b17314664295d2d49" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[1]", + "fe_be_hex": "6249297a890a17fbc31920e6f5b33e284b17b738d945b93af9ac63581f39e926" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[2]", + "fe_be_hex": "533aa4f8f4c2d59ee7d3ef836d846c08608c2c2a0f9322f6f6b269284401b569" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[3]", + "fe_be_hex": "0a7069537b5283f647e1a5fb4510e338d37915aadcab9cee9ac42474f027159e" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[4]", + "fe_be_hex": "036721bd95b1adffb069c6f37846d0949a2541fddded7dbef574c926ea067125" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[5]", + "fe_be_hex": "151dd83de8726f4907cad27fe7c01b6a38b780c9e36a92f2b14b334c6d43d323" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[6]", + "fe_be_hex": "0d8f561572fb44d01ce808d9b57f3dea1151b1c70af6170efd615597953aab90" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[7]", + "fe_be_hex": "388498775fc06d0cede26ab654db23b72fd6d2c693ee2fcebb0c3c8943544618" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[8]", + "fe_be_hex": "3f4cd7d8912aa56d301ccaabfff5b239f7d0b375dc7e59e70ac83d1df74cea7f" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[9]", + "fe_be_hex": "0d28bbe4bb3217c71d9f55cf4e629b6fcbb9cbaf3db566064f6c14d650c3518b" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[10]", + "fe_be_hex": "6ee5a51f25ab89cecb00e84084a45fc4f644603406662d4ee5e8f8c93ab72c8e" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[11]", + "fe_be_hex": "6aabae683a739b717d4284d4ce090ca897bf16439d18a537381223febcacf0d1" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[12]", + "fe_be_hex": "734189390c0049b73dd5622affb5e10138229f67c4fa0f70f3acc7ac5492da03" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[13]", + "fe_be_hex": "1be1180e1778d1802e467100378415b572888ae535042e01cb3539dd1059352f" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[14]", + "fe_be_hex": "5a8998c3bdc57c8c6bb4b209ce16f2e401655f094e065e08a1b42d14091d6c1b" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[15]", + "fe_be_hex": "497165498e6280b15e3e3b2b3bb1be8fafb4880013a2860237039c2872fb5a1d" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[16]", + "fe_be_hex": "3969731b9c485b627553a8d5c91fee63f06759560d441480d36ab2144c382294" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[0]", + "fe_be_hex": "17e99dd0f4b34d2795f0c82eed3b8081bb499b675d2ef634a8b2b399adf374c3" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[1]", + "fe_be_hex": "434e96f8f1801a73f63d1faf599d28f3c7c9eaa3112979564c2a59a955dd3cd2" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[2]", + "fe_be_hex": "6b5e6841b8101eaa53ccb8eefb7fa79e95275028679301c335ff66f216f41b93" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[3]", + "fe_be_hex": "43752a03cf17d3d0d64777af9a39d283e7859eaac4a2da400baf3af7359a127c" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[4]", + "fe_be_hex": "4ee605378c3ccaf4e48e82fa9f5c6ef2c9bd3a44f482ef00cb170f7901cc2b8b" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[5]", + "fe_be_hex": "0fc1c5e02cfbf5d9115b7137074f6f0f6f294d7777c54d75f5b7c6cca84c03f1" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[6]", + "fe_be_hex": "5492e7d49f48fdb68ab634eaf30059712aa662ac23bc8d7bb269afee222f3d93" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[7]", + "fe_be_hex": "09ede7d5f304404ce3602f10ac6c48423349268c98a5e7512c42a85e54a4a8b5" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[8]", + "fe_be_hex": "25bae27aa445934670e7660234eb483b0855de75f8e0e5c25a8859d5b5f1697a" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[9]", + "fe_be_hex": "10a09e888fe4fa67cc79bcce1fb2965b6e166238799d9772d47a07b947236493" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[10]", + "fe_be_hex": "22b7aea68284a218fc8a1c85c16f2674c1f9b76494e29be3d5d1d25879b2bfe4" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[11]", + "fe_be_hex": "421d73a8c0aaf6b421264b6ac308d0ba2dd267d3f3134b006b8e440d08e2a0cd" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[12]", + "fe_be_hex": "2fcb6e506ea4f6d5a73ff93256c62ef6c08ce5cb4dfa0834f80fcebabbcd98c0" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[13]", + "fe_be_hex": "2a8865c0d658cdc019c2bad26c10b388ab8138c474013d2618a4a7a9fd78a151" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[14]", + "fe_be_hex": "256e955fd4aca98fc22078c6b2ddfa4535dde657f14193bc91a10e81c6ac8637" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[15]", + "fe_be_hex": "3b36739c2dab05e8731283b6a33ae8a8f899a4dce36067d38876c5603e6d4dfa" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[16]", + "fe_be_hex": "71de8b575f2b8d2f97d31a3877fcaec1603135e3648dc8fa91ed9a159dc72026" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[17]", + "fe_be_hex": "2693d914e4ec4b414043dd7cd62baad35ef74a798d63641cbb89b9adaa44b2e5" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_cur", + "fe_be_hex": "6f5d99221a6baa6fdd4a86dccdaecda185b900e389abb4aebe6a78dae9986dd6" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_next", + "fe_be_hex": "09e25e6e94f2d464bc6dfbdb00dec371465c9e63b351f030faa16f2fc9089729" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_last", + "fe_be_hex": "6d7e57bcc8a142e541bd48302d7d5f09ced322a30f7f46f5ebd3f068934ff3c5" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_cur", + "fe_be_hex": "09e6e9d001c1722bef8caaa1defa9016313d4ad09c6ab2a025c873fdf217a88a" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_next", + "fe_be_hex": "3c4fadc803f4d688529db1ab405f4b7c87d62b8312d06b82580e09f83509eaf8" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_last", + "fe_be_hex": "35a66b9743844178d52f221e577f21da84afa99357bd796ab5ea58a44000abaa" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_cur", + "fe_be_hex": "0a1d4b022e51379ad4fb5e3dd2ef2d85782787f64cdc57b69e4eefdea908c5be" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_next", + "fe_be_hex": "04411e7377b1c6c426bfc47ff792dc95b65276cb44d3e3742944dadd2356ecd7" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_last", + "fe_be_hex": "3a8947962b0f02550c0eb3ca374c7dad62e6a93f6beac0b717f0ecd53ddc08db" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_cur", + "fe_be_hex": "530b29213aa759519d799bcfced039200fdc999b52b0a3a805fe5f17a87ec4f1" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_next", + "fe_be_hex": "610e49e355e2d7b519059f2a05cee82ba7af81ccc8521c00c05a299b43afad72" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_last", + "fe_be_hex": "4c4ae9b7ff00533557189e0d7e3d92968a22b735511793c9b1cfd24ddb481426" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_cur", + "fe_be_hex": "360e366879475eaf65c181ee1ec4f7cb7505e72c2cbbf909ba9501657943c7e6" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_next", + "fe_be_hex": "6793d5eb4cab328ec774e8790d4fa5f7e2eb2fcb326aaad5da8d7902bfbb0cbb" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_last", + "fe_be_hex": "61bef43e11955c61a073955b26c7a5100c70f40cbc557d72efd31d37de08eb99" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_cur", + "fe_be_hex": "4f8352e3683141c181c8b3f2c36fb3c87d307f2ce83460476b2abc3e00e367ba" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_next", + "fe_be_hex": "6a3393d0a58d7eff8d70efe2ca1651abea2810066637f38d47d3fe505e6a66b1" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "lookup_m_eval", + "fe_be_hex": "5db5037d269fe3b3163aee34efa1700b945b64eea07a169e05c1beec055879b3" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "lookup_helper_eval", + "fe_be_hex": "739afd93c5d8099da5c1a1a38c3b931c0b9d78ad5ac1b0cc8a5a02fd0b9b8290" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "lookup_acc_eval", + "fe_be_hex": "3c654f49671abd7b7d5e4a2dc444a8b59ba8be9a5422bfc776344bcda00f2575" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "lookup_acc_next_eval", + "fe_be_hex": "5c73faba39c84ccb0f0a9be5f7afad39553d992f22d65e43370137f7f1748ea8" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "lookup_m_eval", + "fe_be_hex": "37b288ce777b7ad710ca063faeca1c7e830994011132bee36592dab105910e30" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "lookup_helper_eval", + "fe_be_hex": "0b10469cb107cf10a1323b9a3488bcb5070d70e57cec1012a0d832cea8f50d5a" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "lookup_acc_eval", + "fe_be_hex": "1f5f07b0f2b3157e0b593ffe0cd4c03602ebba3bce94a97d1416c12c0c04cb13" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "lookup_acc_next_eval", + "fe_be_hex": "19aea3331455c4d6a7d7e0396441b078d62d74d9b13021572a4bd8f9ac98353d" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "trash_eval", + "fe_be_hex": "2c5b5194dadc6558b5486ac297257d2f731ab83bf4a6de7f1adb44611ad985be" + } + }, + { + "kind": "Challenge", + "data": { + "name": "x1", + "fe_be_hex": "13f8c24186a7be1a397c7b2859ff227b2d0920d32d545885da542bbed0ae254f" + } + }, + { + "kind": "Challenge", + "data": { + "name": "x2", + "fe_be_hex": "29175d62cd5f3b273588fab38705600a7614b67768c291d31130891f1199a011" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "f_com", + "eip2537_hex": "a6812bc568c0e0aa3aafc20385685e504c2f36778180e78ca44ab76a64b6be3b77c87f80da055bbd519aba9d5122e459" + } + }, + { + "kind": "Challenge", + "data": { + "name": "x3", + "fe_be_hex": "58a0feaa98295060f65934374141b1b8720018a665345b485d2fc89de8704cde" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "q_eval", + "fe_be_hex": "15874d911e497a01d83006b38e4b7fd697d1cbc335f9119f085739db694a5a85" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "q_eval", + "fe_be_hex": "34d56361507e3fe5e2faf8f8289d4fb0b5a83072b4fe6b898b1fab754381561d" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "q_eval", + "fe_be_hex": "4928e1876a849e1c27ee7536482186bf9359d26c1d347cde0fcf83aa529f147a" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "q_eval", + "fe_be_hex": "48ac5842c24471f2d12739e9db0f841e972ee94549e5a16531e6b0972273c8a2" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "q_eval", + "fe_be_hex": "4ec3466b72d2b0ea1ab18978b23d2da7d01b8b040443598bbe13c6b04d404983" + } + }, + { + "kind": "Challenge", + "data": { + "name": "x4", + "fe_be_hex": "0406be097ef26b66a1727792c8300aca3e81715664c38d1284b4d40ea47edc67" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "pi", + "eip2537_hex": "a9f4920b7a8364d6c85cfc71346be70f46dd9ecd7fd527f2593c83cb55359ae395f091c8c26289f663cbcc7bc945b923" + } + } + ] +} \ No newline at end of file diff --git a/proofs/solidity-verifier/fixtures/ivc/solidity_trace.json b/proofs/solidity-verifier/fixtures/ivc/solidity_trace.json new file mode 100644 index 000000000..287b943bc --- /dev/null +++ b/proofs/solidity-verifier/fixtures/ivc/solidity_trace.json @@ -0,0 +1 @@ +[{"kind":"ReadPoint","tag":"advice","eip2537_hex":"b5a3ed19b468966d517a4246fc2d3dffeff693cfae260ebca3c20570b03d90bb67818f1f29fc5eed276dd931d0a8f744"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"81725d4f826bd4e1b2d44973a7cafaa3ef68635e81b756986e1e7e9c5aa88dee5b132e5675c53cabc1a6650d4f7724b5"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"8405678eca7b3899fe8a5151c2f120af233db43316370079b635b0a1b586a11f88640a51e8fbc3b828a9c5761aa247e8"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"a94e856a5ce18ebd3ac321a8e927365411113a2c94c5e318351aea31e2927b998ed7aa69f7ab94eacfec259a90944f4a"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"9927ed8349c0e9a120abd70a94b2be16d801db78a48ebf1b8e832c12bad6b1439d2449a232fb7834c9ee802a9c17b99a"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"87f25afb4c569da357338c5a8d6f6f8f9a70d3ee4c4c76112ce557120a6c6675e7a050cf6e31a3bf9810ceb691fb5939"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"a901dbf17bda36068a654324f80bbb5cb662cb3b6b6c8de00c092be6d339f7089b92885504c313ce6942de14bb465b5c"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"895a3b7f738bb1df304e5dbe3d3f053598e3eb2e421980ad161aabc942edefac85393611ec42fd40d0374bbf924f405e"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"9662fc7620753324b71ae06218c1b5e8089bb31839463665723d8fbe7a2ad245e5133c774bdf716041751bd486235a32"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"8024ab67e2e9b10695f9d9de724f8150fbe2ae651c15270b46ea58edab90cd9458f2384ba9d3b5fab6645dcda696a95b"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"940bca31980207633c105e4723789eb5f4127c41f6325b855daf2cf3180eeab52b72f734549b72b57f0a27f787a5ca07"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"8cfa3c33b295c80d24437052ff753e93461752b8ab0170a48208d445658e53f6fb76bba84de7344233644bb68650a7d8"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"a7390d9b294df6d8c18c35c3ea28883c1c4cd48fbc12e8d82928d1e2dc340cd64defac67dca106a37967fab7fa8d3cb0"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"acc0d4d4feab2c840d1eecde2bc9f96bc6d2cda78ff298ec6716da118151cf598938b6c9346d8037a02b2e241dd1a3f6"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"8af8bc94ee0178b5f139926856976d0253940d5cb5391b39b9b4cf2c06f3ba7289bd798230be9a834976ae152441886b"},{"kind":"Challenge","tag":"theta","fe_be_hex":"27e8037bbfbfd47ffd3c2ce862e3acf6b81a0a6619471248726ed6576227807f"},{"kind":"ReadPoint","tag":"lookup_mult","eip2537_hex":"8309bd912ca8cf399279d7dbc8f453e873317ebcbc5e510acb13e62904711a628ff30b61dbbf04f66ef56f464056c3f4"},{"kind":"ReadPoint","tag":"lookup_mult","eip2537_hex":"873b6c597af9d183834ecbb4ba28a741e5a76ca027e14dccb5018f7cb527c07bceae9bfbab353f064720b0df70515ff5"},{"kind":"Challenge","tag":"beta","fe_be_hex":"12bc4f0d2f0d0c27bdaf7b29adc5b364ef4eb3550618960d1e6e7699f4b71760"},{"kind":"Challenge","tag":"gamma","fe_be_hex":"32539d02a7dc77ccee137a40521fe9a0adca56d16214e383a1458d03ea2ddd15"},{"kind":"ReadPoint","tag":"perm_product","eip2537_hex":"868f44698ed2b1174d655ade22a1bcabde75534c1af29d4c646df25add46d276e58066d8ffc6d7a7221c93669959d1fe"},{"kind":"ReadPoint","tag":"perm_product","eip2537_hex":"a734a580452233277b40065adb5f3145c701f870e3f8a07cf401ac04ca5787e44b551f11f521813b76db5ab41ce56b83"},{"kind":"ReadPoint","tag":"perm_product","eip2537_hex":"84da1d462aee0e1f8e2b08ceea88ac0b7f60842a6ecd97428bafa652e50b722f646dcc6d5a3b41ad5a5ead0178a9775e"},{"kind":"ReadPoint","tag":"perm_product","eip2537_hex":"88677162fef7cc540a46813c82a43da84b476a8726e6e376faaabe15d30e379aa0a902b5c962cfcad26f0b6b7046df38"},{"kind":"ReadPoint","tag":"perm_product","eip2537_hex":"94ade48a9857fdd229d84e556f6e483c512cdd54dd550503686b8317d6d99fee43f79390e91b5ebd303cf0c6bf32f06a"},{"kind":"ReadPoint","tag":"perm_product","eip2537_hex":"adda51a9ad06282aeaf8d1f2d5e7e7ffdbc3e89f0748db870157cf2ba3de8053d731c204001c8063a3b4e18e0981adb0"},{"kind":"ReadPoint","tag":"lookup","eip2537_hex":"906be3c46e0499a65febbc526c0af1d4c7f0d630af1f76ff05f3da874adfa98f83d8a9d048a94b06bccd71f1d8370e22"},{"kind":"ReadPoint","tag":"lookup","eip2537_hex":"a963d0a8790cec1acad417d575f03d18963b97d0b8699fde81074471ab5ff84b6b2435e4de358646f2f8cbedd20781b9"},{"kind":"ReadPoint","tag":"lookup","eip2537_hex":"8d562aa58c17eb6a5bead6f10b62ca93dde62817d7b1c8d37d60ebc7699f07bdd4c1df103a6308381b6ce1ef62758956"},{"kind":"ReadPoint","tag":"lookup","eip2537_hex":"82aee8ae26dfa6e8cd888e145c7a221fc4b4179bfc7aadccfd173129b1ae29c28f8f8cfd0b68e00142592aaae2697662"},{"kind":"Challenge","tag":"trash_challenge","fe_be_hex":"1f4aa81eea4602bcd9a4d8c413c83d27a205f49d074b3e58d25d8202feae2b60"},{"kind":"ReadPoint","tag":"trashcan","eip2537_hex":"ac938f8075047aec1fa27b4a958db254a15380ba93ec93b19f5cf8a8be1d4078e74b1b1a0a70d907f5ce640816ff1d15"},{"kind":"Challenge","tag":"y","fe_be_hex":"4b77a617a03318cfb24402ba260a8d4b6bb38e03827db22b36d6196840061025"},{"kind":"ReadPoint","tag":"quotient_limb","eip2537_hex":"ad70a4fd9070cbb6a2dd9a302486e2158fcb67de21eb59cee8603396e869f34c7dc3eb5bdfe3cb6a99629ee6524662d0"},{"kind":"ReadPoint","tag":"quotient_limb","eip2537_hex":"a71a43c4506df681ca8112d94d5f57cdeee6c1be7c875f0cfac40e8291bb52dbb147c0655b7c15aef823e5778f764ae7"},{"kind":"ReadPoint","tag":"quotient_limb","eip2537_hex":"b1fe3bf128c226686b753e4bae0a9f7633e3ff0d3905e0dd90e926578fd1d9bfc270e4faec8ea370827f85283fb5231f"},{"kind":"ReadPoint","tag":"quotient_limb","eip2537_hex":"af288e6aba3df8ac49d2320b0d2d8e1bbf9665922e962bf2047178ba018bd55b6c6494c59f6e55db80cef009dd2ecae3"},{"kind":"Challenge","tag":"x","fe_be_hex":"480c85d8d1c587dac3891e48a29ade9ecf1674679a0b808e8d2cb5bb3491f2ea"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"09ef5b8920d5d73deec7f3a353ac34262031dbd8f12384123a56b0444384a71b"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"5492a2956c450bac44d8590d6d8802e03f4951161e6b8b638a70f02fa6f8679e"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"201b58816342ae2f3b644148e9df8dba8e4df21d31e77e57d14c192830856d0a"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"3e8973b8bec854146133131923f26a622f86d10f3729e5a033972672d2ba18aa"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"08476b87f42a92ea6faee7430cf19849167bee80d0903e2b94ff68d20d067e16"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"4a5f88276bae0a6dc5737b3acc98ef978311a886979287c51213f09e50700dbc"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"4ddbb2b1a19d17740886685096269f5cfeedecbb4f842b69e18da0667676652c"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"64b29d580131ef7df38067302827572041f0284568335e3a06a9a73b736e2acc"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"2cff007180182c6e10eb772a9093d7b2f42a77e82888711d3e74bd370b605d03"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"0eb888285a964a03a607bcfdd1ba5ad71072b5e96901bace474d434cbeb1c35a"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"1d1f55b5e070b508ff54a51106ad265408ba62256d10f0aeaed5dc756e33bf51"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"0843b40b6701ff67871c911b0bb6768586ba9c034200dea8eff3dc70bc130b66"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"3f9f6a7efb75d7c4571bc4154df2fa8c95e1df48ac1a1e3b32f0f3920d14bb16"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"19242199f60bd966b08275c90466ac422e5945d0ec08b26fb6178369d6b32abb"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"5be8a576c4e42c8e6698aa6424296843af9218bc4d67105d6c77e792fb936cf7"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"54c32b308570688ccb4a3d9877f1667393f9c775a190d15bd789be77a7872637"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"65dc496a78bb27bea7e832fb2fcc519ca0a1a0e9f1a3da01fb7842400f84596e"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"2fd8a069b0528005bad464a072e383372b46dba2bb12557d1c0620f48520d260"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"2c925fce6a934219fbbab1c5a2423edaa05d161143fc7731319d38ebab1076c0"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"4232af1b388ff9803753312c54e93a055085b754c3f051507f855cef7ab8415a"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"49b4754ff126553764ae10edbd8caa67b7ff08b4acbb4cf9403588bc12e7b9c4"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"62a4bdee4055ebda8b75c8c458c6d8ceedabc9c82abb574ca64d0d0d54265b49"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"53d0c5cdfd35487168e993afcc0857ac972e732b06d3d21e33d60b8153c73d9a"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"2c3fba7975d6454c64a7806254b583692fb94c78221cd2d50256d03e47a71146"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"596f33fe28d30a2cd4c578e366568a4fd773643713d11c317a74d1c324e7fd23"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"049d1ebe2161d109d340ae7152df2f4831ad22fec7548578283d91d7607e6474"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"6627866c47eba59bd8337e7ef01a0067277a257293e77d9cdb1f317de54c7074"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"34f29783304a2e64bdced10de519a3550478203d00d69574dad8df6020cdcd30"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"48362875e1a6d36ba286b3a5a8a538d437762d7364b33c3c3b742b2aa58913e3"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"61935b799327cf51fc7b3e9d3662669d75026e5bd83f8bb50da9c277a5903dfc"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"44c545a9fb7660541a696d877c75b4dc275948679124d98196f050c72b3bb4db"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"27a1235745ef505fe3cc0e473f494379096496a9f239282f5380ea91e9d4c0f2"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"3197b3329a543c53400fa92f0ef912fe3f12265a7a9ebfd1fad1c96c5b4b7573"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"0c6b7da40f51cefedc5e562d996225571e96da7ee9afce17a551f343f6893a21"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"1d219cb7e0e9fe1c009f965566fbfa71aa4567bcd937963627640f6eaa6cc77c"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"1d871e9691901bfd6d4089a6cf77afc86504b1521e3ecca9172bce5984df3c07"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"5ac394b97b804f3b8557fc3b67d2ea12919e27a9f1c33aa3470db54e493f30c1"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"46d90b6372812f62be94add1c0914df911e1c520e0d7776556696a3be3f2a2ee"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"461997adfb8e95e75c697a3975ea365a6667e975e90fc5520d86a2e19a8d5e79"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"019b99c3d5c202bbabc76aa2a2a86c9e445988e698197b96cfb09dfd7311511b"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"14ad4103f2e8bc5ddca276f21b54e3ee4054a6e21b9d567e71a819478626cf80"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"5b6dfb1926f4165466b1c34793247660c1ecd8244a2eab3340aee10747bdf466"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"2414f225327ed2c7ddf7b77b5d10f05d6aa9634913a35c2e5138611de4a6cfc9"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"6ab6d2f1fc1ced14f72c7a8cdcf48abba1e048161d8a720c7a290853956f1e2d"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"6dbc0726ddbd4e1cbaf60f38999498eb2e8eb9d5010f2300594d3703be266658"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"37726ba651383f231e61b5cf282561273a93f0344d01d68d4dc209e724395d1c"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"3e68bc805df454fca334e1720f57a3363811f1c17808da70f205bca79f0d599c"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"3b40f386c432f27daf4e49a626e983cc4a8bb5501f0fef370a463cfed9d63aff"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"5e34b61c1b3db7f63cfd45d3e261d300e858536fd81612be8f50451447b519a2"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"2f16371f1d896cd93e2201b39fc4ba25d2f6d09d8b82ac12e46e930ed3ac21fa"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"0ea3f215bfe32c3166b8abbf36962f3ece721ff0bd303c5c59bf566521bf1791"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"004f497fb64bacf49f0e7273047e805a2c1af696595c259f8a73a49bdb41799c"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"3e06211806b4e7b4674d4a2dd409484190ecb8158387b9d9be9940e35fdf9b1e"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"1db7203289fa5f6db1e2b150057157b1da3fc8b87a54b62f42f3521edaeade54"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"01bbc3918a1edda73c72ef9c0ccc91b584fd1915e469bb9355897c37515e0075"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"1134aa786c389ecc8afb97cc1f80c6d17fe4b0a282371404e99daf7e22748a0a"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"44154629726a79952b69531e24d1c476a100c387864071ce9873ca0bd4fb8259"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"6c9e183607a90e4d710b847ed3082cb90e11430e161e180fb4cf6dff5e60f6fe"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"712d02268b3d16ffd37948ddbf54e8e0476873c1a561ed9a50983d8c3af196a6"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"5839aa24628fdcc4ee441615e0f60859084e903ca95b43bd89aee00ff1accc76"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"37f5ac771c0edd9ef214a34b33a469bbac297bc17e16ba72be1c046fe41fbaae"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"655ecb16378f8d990cf6c442d21bc690ab2ce050f6af93c8d94357f3979e72e0"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"3241d62aa2ee77caa09d00c1048f1b383f5f632cc741a71a382d6912a5f0350e"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"373f161bec38765b8579fae5f9057301be67013bb38928b5776909ea08337edb"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"0dc1d24a4f7bd8331c394efe98e5f452ff3955e737ddae522f9e2552ca79280b"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"516c5fae7051b044cc4798c9f75bbcf06c4b5910f882f1ffe15e82f329165c6c"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"3de4833d15a75e94c5fc30c03ec19c7e238058d2fa242e7caf17d6b7b57ee2a1"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"201cd483ad4dc7d466cb4e9e2ea6785b921cc491bfb23d121a8a4bd1c314af48"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"18f0620cfe7ecbea2b26b6419cf1afa321bef2146fd0e26398d201b2b68cefe7"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"0a2b19de67150f439452406bb4d962da831d80241e625cd714e9bd0a413ffc1a"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"19a2b386a5a2414caf2d9a688b4bb38364810775cc9fde2ed1804d34a1e4905e"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"4729b5d831af837bbb9f838f11dce195a82d1694320c7e9e98808e77b5fe556c"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"6307e640d82663d46481590a917b746fb0627a5f80dec829c691d0a442dead1f"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"64ff573b09c5d6897d782bb730572d1f05f73ae3c9ab8b7dcef6a8312df95c23"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"6b8bd275e850818bc69ff00f3b8be5d689eb8cf7be45af436d2317ad048d8a73"},{"kind":"ReadScalar","tag":"perm_cur","fe_be_hex":"69f0d9446331009d2881069dc939974374d361c058e4419736d747d753d8e4d5"},{"kind":"ReadScalar","tag":"perm_next","fe_be_hex":"5f14c13215ffc96f43762265ec6be1523aa467caaa8dbbed4fb5250f6f5fd26b"},{"kind":"ReadScalar","tag":"perm_last","fe_be_hex":"08c5f4d6099af4b9b425951caeec5b2034cc116c781005a1855c1b0809acca87"},{"kind":"ReadScalar","tag":"perm_cur","fe_be_hex":"09da2149c10bf24f5398bb9ffce65639c62f5e65eb46c9088527dfb3058f0281"},{"kind":"ReadScalar","tag":"perm_next","fe_be_hex":"324f6ef7243ba96253f8669000cdacc095449ab947719ea8de02b57ef087da6b"},{"kind":"ReadScalar","tag":"perm_last","fe_be_hex":"503c69e9a786ddd9cc47b1a7b33c24d9125ee2d3a8a73812ddfeb2e2820c5b81"},{"kind":"ReadScalar","tag":"perm_cur","fe_be_hex":"3b45aab1197f9e693e77ffbbf656671c103e7cc7fabd7676cfba7c0dbe1e6910"},{"kind":"ReadScalar","tag":"perm_next","fe_be_hex":"68038ff1509fd4631d2d8ca7a62939990052c92827ae6692656bd3c2e18ae3a5"},{"kind":"ReadScalar","tag":"perm_last","fe_be_hex":"52f75632c182416b0ec8eae43b99ba8f4cb07f305d4a23e7328433ea7cdcccaf"},{"kind":"ReadScalar","tag":"perm_cur","fe_be_hex":"5c3379b067d47d07f0baee88a7f96a71809523bc2122e94ce803a77bd790b7d1"},{"kind":"ReadScalar","tag":"perm_next","fe_be_hex":"142f5d5ffedf93ab9733f1d1c1322d7a2a787636328b35c11e0e9336c9776233"},{"kind":"ReadScalar","tag":"perm_last","fe_be_hex":"1dd4d02fab4470903c87e221cbef954679ffd8190fa9df7d19ccfa9730c1d32d"},{"kind":"ReadScalar","tag":"perm_cur","fe_be_hex":"0311fe738e056e8db829dbb795c41570a66b8de508afba7753313a8e0f64b1f2"},{"kind":"ReadScalar","tag":"perm_next","fe_be_hex":"5296e8474e56d657c049421b19587912eedfe9a88ac5f3167eb63ddd4bcec4fb"},{"kind":"ReadScalar","tag":"perm_last","fe_be_hex":"04263ab0bcb0df4e05cab8ad7e3033bf7a14d5b0bc2bce18deacff2e36cd1741"},{"kind":"ReadScalar","tag":"perm_cur","fe_be_hex":"24cdd547d581885a9499b9dcfeb3daf3f6b2573249433b68ae4b06e5c6e17145"},{"kind":"ReadScalar","tag":"perm_next","fe_be_hex":"066b9b55e4b6942a9bd6a7d37d970a88f38772d00e3955947d7288267dacbdc3"},{"kind":"ReadScalar","tag":"lookup_eval","fe_be_hex":"68ee03441d1c0f093ed8fabea2015e3444c1d9ae728a6fe6da42fb84a98a8e73"},{"kind":"ReadScalar","tag":"lookup_eval","fe_be_hex":"18425279cb0d2ea5a3fe6a9008003baf88d8609655e501bb056d6fac2b3a8154"},{"kind":"ReadScalar","tag":"lookup_eval","fe_be_hex":"08f9d736bb32f4fc2fd22f40ce8d737e27c02aef5ecadd2117e182ccfec4907d"},{"kind":"ReadScalar","tag":"lookup_eval","fe_be_hex":"52cf1481bbb3ab0dcb051cff44bc54d8719152970ff508290418e0c36d59ac9c"},{"kind":"ReadScalar","tag":"lookup_eval","fe_be_hex":"160dae4607f1df78f3391e992a9fe6f757227067b7ec1d0d02ec14c1d9bee8db"},{"kind":"ReadScalar","tag":"lookup_eval","fe_be_hex":"44cb92fc492e5dc73f69d9cd84c668734a36de040e2b9fd5f158fca36f9c3f10"},{"kind":"ReadScalar","tag":"lookup_eval","fe_be_hex":"1ca147219543e05d489c2a27ea73f94cff979d018a579df61bfe1fc962913b33"},{"kind":"ReadScalar","tag":"lookup_eval","fe_be_hex":"0a163b1652cc1a06d13c3d35fa936ddf34bbc1ca841c6c39204a133da267c0ac"},{"kind":"ReadScalar","tag":"trash_eval","fe_be_hex":"2236f4d3167396b4d87b1d7defd0e879ae415244035f3d7eaff75943bb4ad383"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4d84f602435f12f8f5b1345a446e1d5af680c023f2e1bec4cd2971d49c4ca92a"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"38e4c99b63fb2f92a207528b53ec6384413939f13d1d5eeb6e40d401095cccf9"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"6a115d18c53d7506b6e98cccbce5760ac0103a70bde718afc76d989e92a49d61"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"11e74c9d92f5333b73311b9d81ddea08e8048e9693c54c3f7f522886cd4fe73f"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4b6767e53ed2bfe61aec89506ebb5d11422fa20f171424c75f1a578e5f14499b"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"39df664536219ae8c6d13cd65b0542a4f37585fc551cc32da9e8bf19b619a325"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0493e84246159d9a0c5ee0b0d451fbafce46f9bb9a5c2845327628d26f1e7bbb"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1cd2ab993c64e766697fe1b0129938a0944298703452af46be29d5522aa88970"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5619c9c33806fb0e1f45ca1279b1c6303b9aa577ffceb8fd1ce85debe63e8b23"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4cc558c2bd77616304b3fb7b7d77a5e9b15abe455dde10e63182f80335a95e5f"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"689634165a2730277593e57798b7c066dc5186379d0b15d4f4d7856982daffcc"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"355b92c70ec6af51ebf804e565c7d1a1228564101c77f73abe0c3c88aeaba470"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"461f03079bbefd53cecb5f46fb80476a0195d44d635ee14f9b29097c0a631efc"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2ad47773f05f81e86ad287bea69ff972ac8ff4af5fb9b08d585309ad53f6daa7"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"57261f6222454259b1b70bf4b5ddb3e090b3900b7efa6a8b536d45a4518d9724"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"057ac9a6d8cdf2c93f6014f0f10b9a2002bfd4efd1a98dda624e3b5f17e94834"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"26602d5b7d46eae0be328ee647587ffc128d4bdf58d5347e22aeb4f828c4d248"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"381b2bcb3e25616c0707c9d717197f9fd63a65037515d07211b05f3d6c9f58ae"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"6ca1d68e1a6b7792a57bfb3b936070ec9e2b62dfd80edf6bb011736de7a474bf"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"42ef248d0a8c8391546451c282110ab5048f334383c82ec31b444d4ceb3096ce"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"55e21e3da2832df2f41d6a6f43a020f3f4bd2ab7bece259d999bc908f0abec2c"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1c53991259ba6373b5823de5dfd510952bac4a31af688a42635a506aa3d1799f"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0f79a34578494b0e29f69a1df1939463ec999607412f22275623c241bd3c3fee"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"04aa7cd595d0166bddec5fe573ed2c525ad1227e5476c7def868f5b264954750"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"18676fbe593dcd108833fc5da5423a9f916fa9a30bd080fbdd93315cd4d47dbc"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"131071e42b01b53cc5a69662719d931d7d7b9990e703a53ae84096d048f9ae3c"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"01675aadf0c0d8681f8d27bc981bd2d2111820e15c7fdf04009c6e0af1f455c5"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5e27b5e1f5a3260b0700251b06342a148fff1c9ac5ad4432f0d95c3c658c56fa"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2c97d440a8c86cc5ac8ae887b62f78ba1adeec390673cee6fdd679465c0794d1"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1824e3e45ab7c6b5353d2d250fdad905e8465daa05c2a47572b791f92cd14ff5"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0f80a9f7bc25fe7c2b512bfd60b4823141eef42adf188c14242f8bf78f6c9fbd"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"6940b7e93ea186db8c9089f0730eabea3b14a6f2bcc4d5f640e79a3df0e9629f"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"17fd958f28b995d154f198bc0da303f14ca5f808705a77dfd144ed6a94d9249e"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5f0ae1389418a52788093ba4a333c9fe08f8ad8625d3d0f126ff9593deee4a98"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"48ffcedfc3681408df77ecf6e1b6b1010e76eedecb534506435060091ecd2625"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"44046630593df43fc89df0f40ef2825e44d3060718c5236f3766852e595e1d65"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"22201493963df06b078882d59ef1baf520c7f8a38e683270f208fe3f11b5da1c"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"01bad9a7a03e41cd6bd172f3fc4e62fa729a77cf2e722733d242526d2f5a7ecd"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"12cc9895e0990ea45556a45312c508ae9fe8f8573484156ef20db108976a284b"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"216b01ea2caf761404161e279de3dc4f62da3a8dd4f469706ce0a1bb3b61448b"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1b67dd2a644fc0d81bf61ca12d534a66a0220548880d3daa2dd459dae236b0de"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"565997706ebffb9d7754038a6c62645854f0df8090e0708df29c899e99022b56"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2b6581101163f525d224824c36435408e6fc905e3f317353314613def0a07518"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"68e20f87934b626a2d5f7c209d869401839a4a118b5bb8b178ce3090d20e8487"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"21d1f9e69bd479903c92a4303b730a30323e07b9bd09962714fc3e7287beb409"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"45ba1a715ae585a0893569cf924a6b7489ae3f09e258430d2b339b8ccde77007"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"280ad47f4e418e50f544e959e498e91b26f1c0d6f52338b60acda2c062ebdf30"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"6ff36b9a1df788dcb81f4f9a55e7b03fa49fc3b7e6d7c25316cdbd2014f48e81"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"04dbf5bc21710d87e2f53f794c449b940c3dbf98d0c3715039be44948efee6e3"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4cce456370593b4b2c12338ba432d7d2dc8d2b954fc27ad7839953b388c7fa40"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"245136107ff6c9127cd806b4c4d551c870e803b2d6bcac2a4b8488326f5dde50"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"037537ee588437badb0e3f22d0d7bfc2cc529bd03dd760e06bfaa4814caa14ed"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"06bdcd39f7b0136caabbea18e942888444142bde27356b103aeed6b9b10613a7"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4938b50ea746606c644b6de8e69de0a1e1864b137526b661ea92e5f23e862012"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2d42164ca6c6c3fba64ca662762968296b76eaefdbcc2f72ce6c659730f06303"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2cf5ed389a2b092dde54c11fc598b5296f3f9ea6083366b8e08af58d2424797a"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5e2e4955bb1026354983477b6391a75eef3ec35abc89ca0ad9b2836bbe052efd"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"23b0a43901f778461e3e19850f0a644639f5b098404462b28f9eb11dcf5c943d"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"42c426784977ee16629b7b136ffd514988b085edb2b2b5732ff6ddef06212b65"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"510c8c528c8aaefc6e7ee6636a68fe5c0d6676244c76e739f6db4350657ee7db"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1f5fa161bd17eb5cd7f51486008198a2288cfc068de4c1d52f7ce9165dbd1a32"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"723089e8c78223ff6bcf157f5d7795048d0e5f342272e157c99fca5221b91c9f"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"3819ad665aa0f4079328407614b2763c879aa0688462dd1d2a054a692fd625db"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"65714decc38fc058c4c9647ba3bda9deaa1a66c396f0a2ff9882db4e4bb6eed5"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"11a51b2754bd7e318250260b9e937f8097aa6f5c456ac5ba7b108dec1f4e94b6"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2b94f00cf585aac715f80005811db45996cb13a743397362fcf9c66575e11c79"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0a685a3e3bca3ff7b2c6a384a0bf9f35d9a2dcdc359de738765268cb3168cb67"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"44ea929ce6e2d770840cd5e995b12643282cae1749c019df2a74fa328152b2d1"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"45c2845b5844106df6f399279ac4df7145f86caaabafdc21285923a6a74179b3"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4d41d079b8ccb0700381f9cd3445420ce2f7216919d2476d167e16ce020ba1d3"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4050fc9352e2344291a6150266d4ee85b3f348bf9e048d5b959cb418244e9d1c"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"241730773066d495dc9d78b9ae20bb88625f7db54fe8922d10cf63d3f1faeafe"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"10c4dee4dd83ea316988a170a4b5f98b098131f919bba00b0b7bd458fcd8da12"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"34fe8bc397563d8317fbe40dde895fdbffb7c69d9c11cc24330d41b9b4db242b"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1186fce670c53ea565b4cc9bedd44983fa02f3ed77004f78630248955dd8fcbf"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2c9d92613f842529943da94911cd4d53d89144aad4f196d65f9282d774ebe3ef"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"43c252a5adfbab1141b203505fe28a11d67c3578e268e74ec5cea27ff466fa8a"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0aec0d131103b9c6e998dbc5e145158df9b668512ea007b84c8d2a69589e57c6"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"16fdd35187250871cabce08b82f94ad2a6b5e64038cebc13c3a193b2aed03525"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"52eb863553792fd28628cbe7c673c265f0200c836e8bbb3a3ad10e9dd2788f4d"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"044b950152a054968cadfa9f38f136dafcde35c8b00c1ef07231244c04ba0a44"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"461715185e0141c19d67c47351d7ad78683e1ef1b5b1d2fb337f3e0ec3f3b249"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"3a6860e88b5723601efcdd3c47c13c7cd78f0f1b718369b068c8c8dff92ef071"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"6f180c1c3141f5d42fa4cdb774114b9a7032ef944c6a0199443f966f63c2a1e2"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"63c9fc3b50a25e5a8ae95997e46f3c582543251ad166556ac863ac987e6bc10f"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"30d882588ee8f6959b437bccc3d6ed9d2deeca82d24a1f26e2d7f22f6914082c"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"04922982ba64fdfb4fd3d8a4fa3c312ae96ed18a2070d900179d47cf710cba27"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5d7d715c6f859bff0b379c04001acc2755621cf4c9b7d83d0aea9ca516340585"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"193e3bd8a15fd20590d839dcded732f17f97d47434bffc3831de95caedd7bf8f"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"152104cd9b83bc618b92993d360b4d04e7478fff837cd4af8da3e64aa9c19f3c"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"447f5f3b822ad2157c6cd978078607526759f0f312beb0cf8753ae893f7bc736"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"35a49b4552bc2897670723e74f0e687ee1cab24d67a67711f030e305d8852c51"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1c23439d4cf76c447fb8afd031d562c07f75d0e5e5c91950c0e4528ed7b3110c"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5aa38b685836bb01905355f1faaa3eb0bf44091f957fedb8866431b50ca21ddb"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2fb774f3e1d4ca762d402be85203ceff34ddeab9e654270864c40042cf693ae6"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4220c142b4b52f30d04f8c217fcc3f59eebbea5780acbcb6189586b586e9f4ae"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1f72922c6c67a07ac1a4ab00aeba2a2d206a979104f8332a97a42a1ba43f1914"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"378647b10cb904a603e7d8f71c01f8ae0bb3b3625d9fa395c4bb52e839cd6efd"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"67487522f6a74fa7cef58cbb07683df5491680ef85085fa407036639950ae5d1"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"19937dfeefd2158ca0bc9e683b1d325bb5c90053db94f517c6ceb9af4eb40c86"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"58745b577b25297b5690da2a6f756362e2886372b5728c9179f4009b92597fb5"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"245ab486accc627b29a57d4bb9cdc50562eb3e8f2e013c55dde35f1a4c4afc85"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"01d265e5bc50c71b71644720d66cae2fcadfcfb9cd57bad131cfd436f16b3ad4"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"3cae55f63a45e8ff3ecc07611f0de9d5091af977e5a9e37b00912cc8f65450d7"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4562d249ab3ab25956c3de8943594e55aecd3e5c3415883a06ee8194a04ecd77"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"3178d11e65909267934d86293bb734ac15e64585f69abfc3e8a96ac96efd5443"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"6626afb1b7f2e317e5ee2112695b983b77158e0047d819aaf887ff0c6fd64422"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"353fd5b33bdaa47a4edf336f9ae45d83385458c9e6d038b44090bc38a29b05de"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"48996b81338a8b87f989f69eeacecba348d0bcd96e5b9dfc78ec615ff3f572e4"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0896da0029fc2c69b7558bb1b17603cf24b45717eb7bb1af86712ba784679e18"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"130fb75572e05392617c3f551305841d859e7e066fc9dfdad0ef4df6ebb8cb5a"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"49c9e00faaa7ed77efe0a387f3c5071503e0e530e83fe1356b42aa564f62e9f4"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"60cef6247d3feb13cdd27653bf4fafa8401af878a97e0a8e5dff84f26b147588"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"07e3202eb6b6e74418ee84c8323af991b2667a27b9a3da4b3913b67a3acc3ef3"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"3935077739ab4d47a0618aef4f8e5d30b10b37f9339175d9eb1b636be268b2fd"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5862cf4d8acb32ed43215038ef1c7e21aef91bb8a36a55f8109900bfb155375b"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"73988db21c1f117f899f4c54bdf532c9b9cb10265f18300aec5a91a6fb107812"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1947314d727a59bfd9e04162b2e12cd90e4cd82dfd8de26e9dc97b7e78ece0be"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0ac408ecdb6ad3b5cf54c15edbfaac8e6e201db89f94a4746d3b13175cf8960d"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"48f98be7724694687e4fc7e16870109ba6aacfc31aecca40ef927bdae0c339db"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"16bbe36a02d0e3e8e9f01fb110230fbdacdb0707e98ee0a54a8eacd0d907051a"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"37ab84231048f467c2b936aa5d14e2104c0d35e1ddc0abcb9e39891eaa640372"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2d354479ac28e801fc69ddbd171916a382bb4d9fe19b7d7e5e47b3993d775c29"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5289eb6c95e57d1ad45e356ff215b710b00977e6b878bac2f3666c337eff957c"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"3c2940471aea3e0ce4cb460fa12b31443bb9d2bcf6761b7e0d13334357c973b5"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"04ec541aaf70d69c8653191b4536b5d179005c975df40ffcefa000785269d1b0"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4c0c5a5528bebc895414052346322a486f3c13ea28ab92d8f3e6895440212bee"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"457d318a5dfcd57055dfe70341eb983de6d319e2865f580cd83838c1a35d0273"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"72f512e290a77f23ffb69963813bfc2889a0708c02478b163cf1ee0589c2c059"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"289bd7dd96c36c5a2cd05941aad06beda7473df7a2e09ecc0292ca3f4191ea7a"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2124419ef77feab36d68c55819258cd114a1ee496fdb8e247bb9002e403cb392"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5682cd00f9040a55c6f616db6ee35678fe44561fad92131a134ef9f6445c9e9b"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4c279fbc9241e8ffc73217187428cbf39cb29dbc86a25a8bab3153146bc562bf"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"606022c0776c73bdba99263ba976580e3fa453dbc6ed3ce321495518cfac5c97"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1b3c85c78620750db6865c0ab7c02962e13bbafd26a8f62e680a06bf099e96a3"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4e88df3cf58488335c62ab4b60010b0ad3e7ec5c6fa8e1fbc819440076f39fb2"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2128334d48bb8c736754aec52e94822a683250edbca9c64bc71763b41cb5f0db"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2efa47645723e1b1d2aba11a901c5ae5c6a3954f2da8143eda496296461638cb"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0dedea303cf72dd88c5dd088f6b64a11ff0ad7e03808cc329077cbce08ccdcb0"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"70178e70b89186bff3ef1f778729fcf11e9909d1a341fc391cca61ae7503bea9"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5506ba47f40b9142d10acfb4678db90e687060b17d1a11c3f07fa37eb759b2f9"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"433bfa74fa0fe35dc664aa104f8618ab10eb6bb09d9e200e87a5aa78560d72fb"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5c141c77fd15a20905f40a4f46a47c782cd4b9b0246cddc79e1b5133721ec369"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"50425855791ad3327346bb7dd650c040a41e909d0e9e826d00850dcab138439b"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0ab7145a5f012039efbd8abc425d1adc640625fe6ad438845382d418af87d0cc"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"52e36072d5fc448de5583f545c97443b9722b6fd380a0abf76bd27d629252b35"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4513c98e9becd4d01b9d9734382c0116b59583474a487c2087da3c3498c1ffd9"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"37d2f2356a2f0eee375547232c36c6756249547d8c867acc93ec1963aa3c6d31"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"3c3fabeb32f89129ad9027840b32aa28686e0627135772c7b6b9510848947a00"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"48aa721a6af9f92c658a39617590780619dbefec5e31794a1358907354d5ff2c"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5cc0ab5e75c80c54d6001fb4e90f302e3269c87afe4166f62fde2965e7fdf7c1"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"6abc22d42dfa2da89411aa00d98aac6a68d325dc34455a6c3cc98ceab56c5836"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"55f48be93ffd19a77df336ecf30080e84669468f92216bf063ed907addb560f5"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1123b770b00b6e62a0f3c01c42d570127854425c70a66e004b6b7ebdeea69817"},{"kind":"Challenge","tag":"x1","fe_be_hex":"1c098ae6b043e3086251d5a69213433f867bce3584401f06632376bf4cc0d51f"},{"kind":"Challenge","tag":"x2","fe_be_hex":"450e371d88aee670e1855a0118a6ef70867a135a675e6283dc8a18cc1384add8"},{"kind":"ReadPoint","tag":"f_com","eip2537_hex":"909974aed62735634516ad3d04fb1c75814b1f04420e7ba5153e3f8ea730a425fd8cc7f4b9c06bc994f9b9fb18153ccd"},{"kind":"Challenge","tag":"x3","fe_be_hex":"2f336b860308ad449b601a9a51554f543475b39cd5647fee3fe190cf524b9693"},{"kind":"ReadScalar","tag":"q_eval","fe_be_hex":"3c84ab264411455250488b3d02d4498a82654a620a29d7f86a53d62c3b7cdd10"},{"kind":"Challenge","tag":"x4","fe_be_hex":"52e5456496a5abf0c1067996c62310f623e7d6b4972e9ae2746606965965c719"},{"kind":"ReadPoint","tag":"pi","eip2537_hex":"8b260bc8f81180fc5b6bce2401c2d81895ef4e29d8ac2300fad888e667cdbd286708865ad384a4aa27bd090951cf7660"}] \ No newline at end of file diff --git a/proofs/solidity-verifier/fixtures/ivc/vk.bin b/proofs/solidity-verifier/fixtures/ivc/vk.bin new file mode 100644 index 0000000000000000000000000000000000000000..670ff28e0213779ab0a81c3866bb6041da154306 GIT binary patch literal 66731 zcmds=1zc23`~G)nM8W_B5eWko6BAKUQ3+dYu~6(zOav6W1-k>g6-C7k6uYn$yRa4W zcb#F+#@TzB^?iLF-~WF{QOu~my<{R6u-z1y*E#QBZs(>G4Fd+O(KqM<|m$sJ#ApR!0MJj~d? zx$v(wo_X;!V4uija=aSjnGH{KJU#Htfv51j@D#q!iKhvkMtF+p3~@|8@^taLetAa2 zm3iCme0AxH_i_(;>1`u|UMIc?3Ht14?J(B2YV*ZM&n*hr7?y}9Au z$I0gpo!L@qN?gh3=(-aVNTV&!SEf+wN&F>x##yXD;r3<8p>~UKwTgqJN8;m+WS2o$#$h zDQWq+2F1qibDU|F-r>UWGsEKR$}VjxbnaS@xM9uj`uDDN?n=<;?a5!y9(-^7?3~e{ z-GSw$>5bisWe*?LYE8@P<2LMi-DU3bJvq8P-fQ)vLa`MdUAE6nkGWj>;q%~|Zd*Q2 zJUOjmyI^Vh+{J6v?$GS8>89Ij3dhS!?yCG`b3nJ-r`ENLt#bQTS)XsK8!tQ1e`<$9 zvww8VbN24sCerkFhYqevobzP4LB0H+YE-+wCs)kZ?^miuKCfNRzYX3pCy*Sa(7;4x`>8=D%k_7_ZkxZBJQ+BDTZW{p#k2a#3xet(^-O3R&1%JrB3 zIMd~jP10Feo4e~)J-kpwn%E4yCb@g*OD$iA znwvRPtLGqdpS@-IlX-bl+XRf3rq5P)*UfeBs$Bi%*r}44=aSt$isY(3;d8>)XqV^S zE`1hxF7_{zVY$ty6?Qd&$`nh{l&!jdrI|AOYeVS&)Sdg zOXeQ>F8{&(&WkL1xAZQsXm_8vt?asa6xK+LH}WsztTz1kxaYI{S9+JvMQ)U>7_~Jp zx==y0W>GT-`ab^f?0Tq8qtUzlliv76e(90VOPW4+*k|W29|~{n6WTnc)03WkI`=F% zx6|~*H?MC+#CFR)#P<5AHzU6Ns9xsyr5;K7%u)|X)0^hMeJJ-+tAkCQ0w?!!t$kzM zK(~aY$)ye_$xUinzO7e&kC|7m#~nR_inKI8w5QmMfztGr7YYvO?>4>Yk|~ouS%g=r zx2X8ClF1*2T(Es(e}7?_XRSXZTGrpR;eAW1VqMRznK?H_n%?ZH)v_1W(+szA>Ki{KmHNi}REtKaS-O>dR<&AeLYX#-B) z^w>M2>X47$KW?eh`rhqYA>LDKb%?vzy-sVl$j^nE95H`3_fmzSUA6iTi(#kaVfSp$ zehp|duX5O{Ya`D3%j!fFk80#ne0EZr!?@M;Q?vQyA3N;n%9zb_L$|$>mTzFwW(?Y8i}9WEcYmX*EMsAE{plnS#)ti3gC%G%(ss~xsge5sXw`ioQj z7MV@nnPK^EP|5*UZ?nFML8+5wK8Q&;~03T`kZ2gPf z`sR~RBA;(tqw*P^OQPbh*Qi`!#JZx9C11#jB{)AC|6|9cW8*G2_j>fabFH$bTIt7z zPS|k7uVK-<#d|F7GWAaR6%Ix_ef&Mn&%Aei_{!9aZ+q9+^KEl_;8DVG8R$5=nkg#iw`t(d1aJ#_Fk|R%i{3Lt3%VOcl{b_@$+%MYg$gYG7{#&m) zj9K6ATZgL^7gh_ErnkCeHY;Q7f%Np+0c9qeR!D7r+OtDd%v!oIH=v$*_{FsD?H zfT>>b{wBfRQv#}dm!>z{vGb!v+$D$B`~2el3dk?sZZJ5Jx!7gQ6 zqWhV7hDU#{7_;VAsSIg)BhU41x-VN(Evn@3ihE4DTiri==4SO#+58IsoVaOEvyv&R zT>Dt}jpA@q)OtQRO_xP0GEuW4+X! z2}7SBtYoyHxWjCxP4Rw71}CNI?W;~Zv|;hWt7$%4f>)W@F8}OSXxXJ3+aD|}*)_wd zt8GGP-|>5Eg93i^(HN?ElqEcdSIIA&ym?qy(^vTT#avGcN{!cmUD}mH|ocF zpUou)?}~1c^G8X$kq`Hl-TrBo-6bJr)Oh4+A6s^1Nbm5Mt;~my8PoCbr>)1D`SpL@ z#`wM6hmQ+8*YJ5Z>)P~{@nzeYMn~Gt*ttR@G2Y~E^Q>RE0;XaE+t+{3Yqu0 z?9b`0-a&mgHA}J$UX}mBw4yGf4|~3Iimo%XVuNdAr0K0p+@IucQ2Bb!0x53!uWlap zq3P+2Gj;NKpYF9Yrhi&N->^a}I{0iDR&P{??v6v{0jbjTrn?fOtSjA%DACyU?TaR3 zD<7$P+|e-h^v#Y=U2R;NADa`KdM9%1k0C+!DQylLml@Vonm+scdONQENVdBtOHAIe zYVeU9-+yeFIB$KA*mj+RR-S57|8mW)&jVddJ(mvXKl?*?jf>Lsx!+}ku75Ya?YT#F zJ1N0%(mEba_M-GLjUw$7SRdM~iDX%u1?|P-jd1-pfj;(So zjb3B?bi{y7TS|Sj?i2p{Y0&7had#)=4{kaoplNZ+n*kj(E_vgBNtLSl|>SG(f+65BaYu&Z74Xyg9Mg5)z4Ge-S?$7>Vf!6h7 z-D=x`)OzEhF16b6KHGs=soC2Vn-gqszfRZ6o95n)zSyD0>K)Om&gAj2iR|_E`{?bzee1uu*a*{F5_% z;MBvEo;Fvd=`Cv<+*fwsn6ZszSMM#WHoHY_|6iAzRc!XEWmxmGgLgiUz9RSbJ{Mx_ zo-bwi-Sr6#i%Zj+Rx=5E6ZCNC*@bO)1y}vvJl;6&+vuK-ZjYvow9a?_h-vk7pXHw; z`*}DgJGI<5BW0m9z2)G8OX``9KRdLwU!xbdr+ppYbX#(8r^xJ2y|1V2Kc2IN)s*O- zWl#Hj>bCGsU*kd>Jf!LKEWBS~<%d(j`yPHAcDG>t@X<~)<~@9CefsCd^2s&7L^$>N z=(}v5XP0AJQ{TjfOsKm|nm$LhnqDP-mb92~)z)F%ouxUO?0U1h%sshlaA<0-Rik1) z>|K%Mzwzp}GkMGo4_-AkW1=*@X}G+8m5V19N67Bp+}Yx1`;kFg!{;?k2wodntU%!G z;@90Ko^h&FtazH!#q=vbp57GZ@){3gpIXZ&y&H0-cc`~)qs@$64ZF9FonNHgfalL^ zzMfU|l3kmm@I}w2cy+s)+H;8Kml$DAukpy`Iof>4<8x=0ma~35b!_+fHCmmnEdQSP zyyL`Fw}f6V*4Dc{dGeMQj{=ge^i6&%?7U+N9Mf zvn5~u-e1y_8dkoX*2UJhpY5K>PrF~W^!etwQ=@Rak-w}`yXhUrSJ&IvuDN`{wO{vE zl^>gZb@}AoYI?@>dXWQe8!zyinXqPV!t)j*y4%EclBSo zgRkmc+dn?8*V0i7>kR7Sv|xIAK;1S+3>}Z3+S%Ot(a{iTdc)3RB7SB&vfg!o{7l)4 z8S9L;&aeE$Ht=#4`wBnox(@a3+pW`-Zw2l|t=?Lte+QXO8EJZx_TNJ)H+g$j?p}Iz z_D}5>^lcMfIC;_f=K1gCy|JNb)WH|m1`K#FB5F3`{1@Yci`Fl9`QVf=b3szWH3xsb+1vN$q)IJD^^U65+wWsBX?mNeyMfD_ zU!S)3)bbI73Kfay*{yTTl?BU}M_;`-C@y|&zi9^;MN8Evm;h@yS96Py)=D} z3K{MC$U~2>cJKFg@2I5CNBTCJdh^B6Hck^m{VvFFwH)HQ`=Vv`mvibjTo4^RVM4ex zy`^K1!iEJrminc{w8#-sx_#8PFR|;aT|2~@k1Za#&Hq!+higx!fAJsPXwZkCv2Sut zk*2rqF}zm6AMOsKhT@k1D zBO)$eywEyOn%>y7LP~Ur*Q1YrFMX`k+I+2DT?^E!?cKTS$41*KHV$%(xbe8YW34xF zt*Te**YSAj;+tCO*RS02aZ`?)^*!dbp3^X}rb}wMje{D`Dfqo|tJ0HfiXL=c8(O?V zhudSP`a1krQYcqGY5JTS`XBn`aVBoi;*+Jkzos{JJiM&-^j3DBm&UlvcMWb(zg%wT zQ^jB1t34**QKO@er=63gw_Os}f5*Pb1?S3cZ)_0#$lB!A(w~F;hvfY>GkojjB1G>9zg4T=VVdTx(v} zeLG&~HjL{Sw5+4y5VQO_T33{&xAU51dUffjIkO*o&Umx+TJ=5$JOYQUve;g5f09>N zt9PzeZ}MF>|2lZ^l4i#b8m)-Xn!lM{KACe#*^Uz$mK>SdVTfCvH@)SPFZU^wFV&!j z_c6@uVyACe*JxIq;VB2#G&o%6p4R%INtNhzMShi?ACl|dpmoiw^%}qHeL~vA$KG8_ zv>jAw^AG0}si$^#etNsI`|RYKotA#JlGfkKIn|}ms%2m9^=LBxU1g*6EnzpRTrqMk zIwZT@%-S(a4n6%D{$xbJ`U71m-%onfGP~CN%i>6J@r>Xm$vyAtlq40~$w zB-ipy2g_W0USrI`3a$x7hx)at-EGykFLz$g*IK{IbI#p)rmTAVfjwr=?R&P~#1i%e zD>i(VcUr=6_q1+fr*};b`8Dy@bRT!yhWBl<)w%miT7Q!xD@@;2t!j62c8}i6<~#`8 zF}wKUmF2IEIz4#zwwa~RPAc(59`|+HrR_Vn4@(&<+u=SDVrmpUu#ZQ_}?K5rhF8vMLJS(@G`@&57#XBy0%-1}YE z^M~xLHe}1*pviTMYYnQDsNLm2Vc&5D<0}>{SHJKHvzFsa8@#9~O>c95o9lyBGxGI` zjat#C{^%!{YmB)wZrQjqHa)${f1LL6p7Y>dM{|#?nC4L4rd5F7>6X&;)@JsxM(dv* zinKUzAxB}~DW@Cu- zug~EV21CamXq1}XE#HuRyTUi$2s=^DKI3)83ac6x_&m8^`7(pngbcdgV}1DNI+5$7 z>2rOp7ntq(*0XtfUoo+t;96x~C3llvw>R7P?yL53No1R$cL%<^7To1QTkED@mRc_@ zC9Gd%!&A5pgx_u0;3=$$=El<&Pdhw?F(wb5LaZzFSL&?5i)n;((x;G*_!ROJpF+Ok zQ^;R@3gw7Tp*+R&W|&-NC6}4YWpV?V%*|CUGcb_Jg;zrZnStK9pzv2p=jk$A_r5@FRR^rXVvb>*6ZQJ0|y$F?-y8g!C$q)8@hT z)f2A_^lh?t@sJLA?o>D3>3CLAh@jNXRaWJFiMmHmm$+yZ;yPjBM=$$9^Rg$utZTj_ ztzZ9K-w$33Rg^CH0lJ#;(YwRKI^8%>FeW@HcXs`CRGt4Zirj*^Z^!}?uifW-& zpqT*X9W={)>$#d%8hfFB&7y(VdR5zGl02|<&>ZV0Td#eTr<9K0K2Wn-7M>^j*64J6 z|CoIptrj<~7c}76w$g*cYn1%_)y{BEvw*jn)v~@DR&Q0qmS0=V8h^aHVMstPi=7Tp zHT`!yi2pu0bhKlBs#-XWa9-9nUzGzXt6gi>d=OaYLz@u8(s3K*yIKv%mJxDn)byh> zd^M}p<66mu-tKFsv!S6?&*dr_1YRyE=Y6Zj{Tj$+w>G0$) z5o?Wu(xaSz?O%9hYVFj-1_@I$7Tvq4Rju5tG-Ee3S^6RTB2aiR&O|sPS`QIXe8EUl zl2j040#1A@>%YLVqi@{ch>oS>Po$m=O&)oXui3UxTPUHzP7m;U? zA<+}+7jE48NR8b;(nju`6~_FfN+Wm$fc&pkg-bw!dn&53V$Rb-u&6 zzzk%1mX3ZWhkm7lr0PkO4w9;;-1}4W>H~wWC3<%4f45kKp$jf;z3VAlH2T#OB&CR` zCrC;WQBRPRBF{ZLYhN@R@cc{LX-#g9_u1)>?niG$=5$LY+7d`gJD~_9rJYa&lG08n z0!eAlKDPPOKGV-%U;L`~&a(5Z$Kz~*6j~iFe4P*RddB6cR~KU7=8F7;9SZbAm7j@T z`I)l(gouHDsPePWE5Gb+uEO9egasOp0?zAy{oLB**RMT4f8nuiy&HOb$wR9z5OT7U z4S2PtNs&b({k*(4l*m3)U<=7T(bFPTIkpv4;z<_5q9VLbjz9R4~&tjDk`;OjHE_rLOV#R zUsKN$$AWf{S-GU99VF!vp&caUl4mEAnCThz9`1GW4DQ@-lF><>W4U@g73Uq}E)_`- z+Cl1cNtLmn9b{H6sc8pExkP9ONx4L52T8e9xIo^WUQ^Ca91<|wBWm`5ByXKD2caFK zzD}1kwS&ycB{l6JDVGTCASss!?I0ljoXYM-%YuA5Rn&3`@)RI7*9p+aX49d*;xnNtU) z%2m*rOE;yaGxwj5PgT{>K2qqk)KNE0omM(1RkngoYu%KZP8%I@v;Bx_Bdeqx+YsRD zJILr))fH8VxY;IM62}N}v%PlQym!ZNn`TSK1rEM>@ol??4qA<|nh=?*KmOggS+4R; z?MNeTrV$0Liko{J@7=W0fcLRu8#F6qUu;>q79*>t4eRPM+n`*-SwE6D_hcmNQ$pW6 zSM%%EBsTk^Zk_7Xl-;elzKH$2zMpC=pIYha$*2<;pKu2Mg}9j(#^O=2=cN&)b}??| zYZv2YzIHKgmcMi|cE}U*b6l?CbrudjKeV^A;YbVPgzJ-wBv!DgWuK;4Xu%ObMfjeD>uGdq!BlBv?I!7F7dU4WG?ZwgJdr8wS#0X@wJ0w zF7dU4WG?wcZ5jG({j0)LjEbI_R{5f{Ta4YCpl98`7=*;Pe{dmjttxKjYe$sHT;gj7 z$z0-V2gzLGYX`|(;%f)VT;gj7nc0?xb}eM>KhpWls2dyKj}AG|xAle6k<;HbANHi# z7W2ama;V~FzIH^J%q6~dkjy2%c96^^zIKqzCBAl$nQe)y9VBzf(7>vDxkq!>U-YPW z;Q6gu=S~~fy545#de4@Z>$+6i5WVJKh@0u_7?*lxY(#&p?BayDS&wE8N{`e0kH*ck z5aHaq3To)Dl{$sEnXglboB2Bb*|?b&Vjxq*&GgqwokHBq*D1u!e4Wa;S!`KJ=#L(Kyb5%@ z^Pru_e%o<{2YkGndEw~@G(HwiE^gt~VRe+=frgy{+Zm^QF4aE4+QJ|GUB4Uh6DB%p zMQ9$1=Y(-=nxpwW-ie6f3r4bOjuxr-KumynlTfx^K@Ui(bgeA{EKhoYPApbgPi!5e zN+;@ASp#eJwZ~6h)qBs^;CpiCsa!&6oI6)6N*4m+6hG(TN+7jVBNJNogk(fuyt( zeF;cG`yXDu5V|Iv53zqh$DmgzVt}qrXJ(2ojQ;aokNKOJw?1i?5N&QY(xpkbWvLh) z%OPuyt+4Y+eO7bW>h*?+KdWn1$yZuOVOAz=d;A-5rnD~V?-kSQzp|I$uf)F)uh5Lx zn;N&7JL;1|7E@E>wz`iv7FJ=9CpB)1q}nLA=N)qtYSu+)2dUE~Rknh5kXgB;rX3{Z5}_R=Umo(cvW=J|+($o$zE0@%?gQQ#{w1cEvBD8~~Tq3lCq+BAl0)o`(lBRad zy0UUfO*=@+B|JviO>#`a)}u0KxXBVns%(6P%aVLK~gRe+Cfq-5!yjgE)m*6QZ5nNK~gRe<2*>g zB{-mJIg%r>9~ZQ1t(Fi&9By5e3Z(arspT>;Rk`=QdQ_j;D@%#=f!Vr!ljDe2C# z_6(_oZrn^arQV$X^K~|Tc0U;9@IUTab5M+B>m2jHBit z#082RC36rl7qyIW$nq5DAT**#z(_U+;cvPS*V*{nD*l~yHd+`abSjri?I4*; z{4L}lB`(pzP)U_bG@{fFlDWj!4wAXV*A9}o#Mcgzxy08FlDWj+J`YlKiK88BO{^{P zwS#0X@wJ0wF7dU4WG?ZwgJdr8wS#0X@oyaf$z0-V$66C}iLV_bbBV7VBy)+c9VBy! zuN@?FiLV`GW?SOkHUW~k#Mh3sCgu`fJ4ogdUpq+V5??z=<`Q2!$jr9H)ee%m#J^<( zq~MYemi?I4*; zeC;5aOMLAhnM?dW52WZ4M?2Pi?I4*;eC;5aOMLAhnM-`_Ael@2{vITA ziLV`NP0S^}c96^^zIKqzCBAl$%q6~dkjy2%c95BEi96PTWG?ZwW37p~#Mcgzxy08F zlDWj!4wAXV*A6nXEpfGjWG?Z?d60rja6r+mXupV8qM+SX_t(m9Npqb|Os$zv(9H6p zzgFh;hi@f>PTdc&D!;LBOf54pwc1BG{9CucYSu#gow%P(>t=`D=O=7i^nPvUS0OvU z@0$^;-^~uf>X<$5cF^O72f45+2k?(JH=+INeu!u0H_oe2Nf(_tsxl<+#}mI+q&JY^ z`Vuk}{%d6VA*L{pIqG$V~mI&uTQi_Orf}|7?^#n;NBDOMv)Kd|r#8gWr z6oI6)6N*4m+6hGPeIilBy@sq(SOgPh6x_TOynXNhu=g36fGo)DtA7h`97X>Zu5q9@UZwMIb5dgd&iX zc0v(IN;{zlB&D71Oa$%dOa7z(96CqmXI)oET_3mCt*J|s5lxF%6M7}$KZjP`Cb8!C zZj<<%0ji?QBVYdJzJos=IFkw$A1Z|Ym5m2NV{*}1IdPm326$I(O8i>!4GZYCDb*(m zq3%>(pfq<_6wpDbvJ-Y#6x2*(4l*m3)U<=7Tq1VGfuvp1x}XuSQ4>m46_wgSQj-8eJ4l@_sWKL{ zgUre$HSHiNmk8}3DVK;Hfgov@RFzbmcf^%CUD7=7Aa%N=%2v=0GAozVw1cEvBD8~~ zTq3lCq+B9)LxR-llBRYdoo&{!3n9)?>y)ajBcW5qz9l}l>c zktgL6(fC2qE>QzL?UJgJ3hfvcb-JXf9i&c|RM{#9`m9`1(~dkTmk8}3DVK-~A0+LP zs*(!r80>Vqq^TXGPM1{KDhB$jTvF4HJSmq5?I05`^) zkXgB;rX3{Z5}_R=5`^)40c($q^2DtgtwiW_m>u-jN}?=2kCqSzvzIH{r$rEbLjY*NM7 z4w6l(`1(O+HT6`s*#B;a2Q7@{DNa3UM5!GSJ#&e#9VBy!uN@?FiNDPcB<+%<)@J4ogd zUpq+V5`SweNahk>J7Q|)5??z=<`Q2!Nahk>J4ogdUpq+V5??#W%(lec)(euk#Mh4K zo4Lf-4wAXV*A9}o#Mcgzxy08FGP5mlwS#0X@wYUC6kHM_Hw<_G_^u}Us%B-2lPVff zc6z8Kn^f@|KS(yI;x_=0Sxr4P2YPDiNkp$079~?pS{NJX#i=KaD77O`<`Q2!Nahk> zJ4ogdzwv{lU6RzA4fGP1B#2TGhDFgO8c`>iOMLCflexs#4wAXV*A9}o#J})CN?f9a zv4LK6iAI##F)T8d_}W1-m-yO2GMD(;K{A*4+Cegx_?;d|(It*{40g;VzIKqzCBAl$ z%q6~dkjy2%c96^^zIKqzC4PqwlDWj!j=_$(#Mcgzxy08FlDWj!4wAXV*A9}o#Mcfo zvn_E4JCMvJzIF_D%q6~dkjy2%c96^^zIKqzCBAl$nQe)y9VBx}uYq20i7gyveIrZE z0sU`e(dRA}Lq$KP^98qi>VAmb!*AS>;)eY2qt6a)aB|IcCPhueVij*$js_$tNq`K6 zvNIQJG-YY!36d(E*!c|7T)#X8GYOK&6QmhI61!?aQl%4l;+|9^{qh7!l}_x`1*vE0 zxYyHAztTZc^(0CMN!63sCk#^0dg5+Nsx1-DgQOG@^#n;NBI*f}Qbg=$2C1ha+!;x= zWI_>0N;{zlB&D5D1d`HDC;~}oCw6Fq6tuhQ?nT54-&OM}*M5h~=a1gOhv|}bA!2P5 zub5Wo)diV~n?v>bZ!0iSVz=~a6|dE`fn>MzYLVLYRNPRkMQY`V2?$j>(G7tVZ;jW? zRx3}0>*9YPpiUBbf@HS|t7AAOnm9bv@#V+SiCC*q!#8Bc0@ z4sU*G+{b#&O^1As(CU4CCCynT{^^hZ$yildS@rjd4Mu#%4l*m3)U<=7Tq3lCq+B947J;N)QdLr+9rGNzErHb34pOH}s%!=AAhU8w zO*=@+B|5`^)tQhHZ zNmDz>tXxvl4w7<-&<>JviO>#`a*5Cml5&X%0YK_>NmDykp|WyGO*=@+B|{{b+%H&;N)y4oQ1VI;x>{6CNrO(O-`s-F0jG67I}MU zMUp!Ie8~U$TAgH^Q(S>UK+iPNUn?miQ2ad|!XJv^?`bf1b9KWsMwrQ{`T@z2SRKYu z^B5v%L5`AnjF^jN<}6Qf9z!Ea-N=*8WBA%ZvUv<&Kgg_Rdujn2)~l%59-$qi#3fo7 zFqKO*qSOwOxy08FlDWj!4wAXV-_HV)c1coe$kB31f+!V%6kVbbb&|CuzINovT;gj7 z$z0-V2gzLG@8tn0afud&N~&C<5v6vJ%q6~dkjy2%c96^^zIKqzCBAl$%q9LlCXk{_ z9PL=IVpkkrJ4ogdUpq+V5??z=<`Q2!Nahk>J4ogde-9T(<`Q2!)~lFHeC;5aOMLAh znM-`_Ael>i?I4*;eC;4J+Y)#G8c606Upv;Tm`i-^Ael>i?I4*;eC;5aOMLAhGuskZ zJ4ogdfA1bh!6jEWMX#ds<*%;Q(Vc-1>ADIProUE55u#juVj^q3Vj>&ij@v)FM@G$Y zS_kIaddd95e_Bq^Da>Tl>Zf@q)|PS9JVxupFj6v)5p&VZn~~x?hDH<#7|G@_{O(X( ztK)YG|IS(+EsU+Ji7wHIQoFcT$JZ{d)$z59YjynY@V{BBqlK|`HL)$xh*CS&z?e&X z?I2lO;%f)V+7e$oNH!i?I4*; zeC;5aOZ;IFr05byJJze1OMLAhnM-`_Ael>i?I4*;eC;5aOMLAhnM?c-03>sXuN~`E z%q6~dkjy2%c96^^zIKqzCBAl$%q6~dkeO|X8~%V~F7dTvy^6WS*A9}o#Mcgzxy08F zlDWj!4l=VXakYbFF7ZP@kb+C%TAkLN8(V+abgnX~$>|%3Ju|}Hi|K#ohJzk|e+2X4 zKmB(&6rCgSB6cW3^zN$tz4*1VcQt@3Xc*e0;uA4}&?NN94}Xb=z%;XmA%8TiWV83k}93pgbPy7(lJP|zgWpH9VAsxqI8f{ zJ&7&AAoZ*#x@oE{5nGi(Qi_Orf}|7?^#n;NBDN}n)Kd|i@@D{0Kit=E5Ju#wh9{k`JX?tkn5Vpp$<=CRmC{>C7vnStgzh0edSgZxhq&YH?J z-zjwdm0e+4SAk~LWU@aUG)VJTf3Gm&uk0K9OTi4}D^w$pqB>#b4?`4Au>gtcgw>tI zv0~>K)d`EF+KvL0P7OdJsf^P8K+vVk69)?I2)d}DijkVyLF#l#m9b*ySXM5nX-A%v zOT=!9|2jBSRZ*!O!67xE6WT%QbV-%5V&_;^E~#lpo|H?3b{#=1v6BR3mTd`jRh)MO zoKy%+Xa}j&B~9%hvvNsIJ4nhULOV#xB|k# zm{AV@@QC=#PD5p zT?t3R|NFuu&|fQH;s1Qm=U1S=^MAK$)J4ogdUpq+V5??z=<`VzcpCFk_eC-IfnM-`_Ael>i z?I4*;eC;5aOMLAhnM-`_AT!$%_gB9lnM-`_2+NsEeC;5aOMLAhnM-`_Ael>i?I1JT z5?4D&=8|5&_7+^qoM>qM(tG#$3ELLEU)%Xr$jQ@n=picO{X z2zzyLgtulgnYm2(ow9{YmR%;Zl*z1QGHYQaP*xZzfys>IvK+ru#}>jdKFcn=3K$Wg-2C_OY`=$F{2reD_i)SK!>xV~xBflc=67&8_T&f+Sh(VZr_lCs z6&Dsh6Ru{(E4wc6-azr5U1507t}48mDLyw>yoy(6=2eX?LW<8V6|Yu`S8K(q4SPjp zgjaU;%VoKQV{|0)d2$(oP-7QYSxbwR28JafU*r$7dJ`9WtZQhkRmSyGdu^Ip+`YWl z(3pOxu$!#$=LrutABo66Fo#Wv2OmZpn-e&&e!t{XQwKedE!uSHnm(B`^>XI7OQx+m zXl5GKbo5Je>#e7QikZs``ZV=$Dd3ea|NVGq&HQmwytoP5JG`nz)PDJ>`NI8O`}-|y z{;qG8@8dh2+jy{7i_ve5mYzTK4m0=6AEXkFkO--&d(L$));(e6urjUj-jCe~?Nz%1qVd$E)&}r|<1yyy}8=>XM(q&Gx?R-aV#?!=kqP zkNC_Bw}R`LKS-5%qkvGB$(gMuu8;P~xqSb_#}iMyeY&OWn7y{wzD%1xdO?)U?OtWE z13vQysWP|23#qCZAH6#)tkaDH1!KaKf~W3~jrMA9JHyPPYD(EnOYgrrg#AI8KS(7U zu^PJK1PLbxv9AeEPV4-KJ1(D`IO%i90>|Dr*3_EuvOvmwmpM6IeO`L|SUZNWlT*)X zO~#QQOMA^Jx8=v?>t2&Ck8qQ1i~ibgMZsUyHz`!K@g{)poyvBqCp zO?=aK<);_r4d!>s=bn(~g$;djN$Z0+eRA4)+wR?Q);DhJ zt0T)B8gvVqS*cB0)A(g2A2qMFDAwz3ooSe4#svJmJ?OL?H4`&WPC(qTb>8ik4p06PvDP>!J<9pl{)Jbj z)=o`qkT5l4(Y>4G$r0L$?2ue&az?0*TZyPAMosR%cGzL??CX@Do&hmi%AP;q^PseEw%-Mxky^e3w$zwuCM#ld68+CgB@?MK#@6Drf+6`&6 zhDmo{-8E=-@&4W29$rbf{wr$Jx*^p%wwFhzKRWMq?Zok?g)^l)jM=fhO7}(U=L|2G zZ*D#>_s1bCX3yH(`e6Q1(|+1#OsP*x=Nm>z=Nm>y*TXQPBW8w$%7sxN8w~4ZywaiN z$4YCyFFqJoVp-bFUEjOhZgk^)^#-F8x6C+EIFK1u_(_hLg^m{OWIUnYJRV)8lUipBDPEG9^5+q_G!m z7~f{fFuu){VSJkj!}PFO7_ac7K5{XeF$SR;rm(s1^>=H$R@dunz2VFPr=o+q&+$vF z_I8=`6Q3QG>ZDf;gE#0`e7{y)@}i(y;w=9$8x~&+*z}-CXM@cx3*dS1*5l;p2oN%Vzb-^|zkdDtgr_hNc;Jao{KC1v}ZF)0;hrNXpSnNAlAgaxGPW$0qn^Tm9Zl_%6m a%1iSU_Fk)}6(r}v(@_BN5cLSJ^8W+(ed+W7 literal 0 HcmV?d00001 diff --git a/proofs/solidity-verifier/fixtures/poseidon/adversarial_fixtures.bin b/proofs/solidity-verifier/fixtures/poseidon/adversarial_fixtures.bin new file mode 100644 index 0000000000000000000000000000000000000000..3f16b679ceb62545452bed59528fb3e1fa5b1610 GIT binary patch literal 38369 zcmeI*XE>bex(D#lJCW!FAxel|2Z>&TAP9q~Gtqk|y67bcg6O@s5K*EedUPU;7A0DY zUNRx)?DO$_*n6*Q?X%8aYd`aK<~4J@AH2`?yZ-5YM0buSaD0-)@+c6~dmI zVA}aYzvo6vH`2;GZTRf#Hr&d^>nr9!=ygurJMmNTG0t+HW_#B&CHmxA(yGn~ICwln z1(e1jNk+6uX`B}K*RtW0WRI}(v8=dLjcf%PNDfSsb8;AIE4fd&gJAunH+(iP39}5s zuv?6GxiKCpXhO{>MmQ@}{fxWIQ>aB76JuEVgENNk_NQNHm5?V$5%Nf;wGvTLo90&v zV?w<2?!!`w%3d1~bP9`XmBTzkFTL_qDLDv|ZwCG9-JmHIzQVXR4P{>v>~{@GlxLw1 z4xB~iifGdjOcik|Tzp8%mzGFkf$PR?tgM>3kvA7SchRq&ZXnWbtE^|t;KR+OVWxS# zU3NKsF=~cf)KxqwrhHXhAr#g)(!{ayB5Fhy-$FKwM8XFr*`Hc zv&X%*iR>87a5uM?dA$2KXVKGYuZ$yO1c%+f6n+Xy9`slZg`8jKG7i=lC%pQhNQAY> zblq}^H~7FH%Wy(yN$SFiJfE4S&Jv`qAW%6cE~wm#{7r$vF(JfI)Y_Khpb zKAzzHk^H2Htbx(N2^o{mq$BztdEGYKc(M(#z`7gng=R)P3zc5Lo zo;pu?sb7#4(W3J=-w)vV4J)V_8sQoa}Jh`-6_6+x&! zQil5p;VJ0F)EvX?i>C%j_vVT~Vb&S{a77##Zrm^}_b^qrYX3^I$>5mI=3%i}e%|%~ zZ8ptRMJ5}+J0g`!V)F8{Z-;BCYoK_Cw}Iwb?H56+QfcLlDD6A&k6{V1(58U51|KM{ ztT+l^paT7`PRyqfgWIKcAB@{u9xEfy%PTe)%#`P%Shu8VPvtL!iYq(o+rN767Vc@z z;tC7o-FrRWa!+zaoaOU#rz0gLf`Y3QBWC{lJG3;IX2Mm=zTLQxIJ@=@nOf!S2e7-} z5<({BIGrFO(@)jm1hMLUa_(v z6H9|^CcSlYlV)iZXKD=uWkmn+vr6qSIgvoBN5QSWs90soo{JS@?X(LnWkxG2Qi4IF zrW4ul^Mv?~!Kdnvs>0r4iHNLUuByRsX*!k%eOeHcX*5DUElU2<$2j z3-T$^uE#eS6qEWpB8BL@kxDXG9k7i>9+;_ozEa5V)ff%#?Ngex7$=?HDOWaF zM=!rGqU_@qd`Brimwr^%X!yq7?3TH4PXTU|%eyu6+-jv5Y6t})#7^$~MH4OMhEAtg zM3M}Nr6tbO)gp%SVP{p374`YbL;)Dlm%RxRjBllQ<7mzHoM?73BJI)SmhcJ@UNKC0 zbet9qd2r+bQ|~9w0v9&4CB%7nq^i&Lj%2QI3AsZmg4KFS%{M}FK~uU^S*pE0(@us{ zW*1|oLDm=}YoVvafSJ-WK(Dcx0(b0eG3gQE!iGTU1zfxOC)w+R1VnT&CV%-m3i+;q zJi4!_QmNJ=?kb-D{ulmtU+VGXN@W2WW0Lg{;~6>Tz|`0W#n2R@?RU6$EpfavX*c(KP&Q-C2l!6A!Lv{&x}s~I8n>cPlI`{xQ)8@;Brh>P z_{>?KbQlP0DRu1TDcWt~y@7JyGW&(D2}GO6BSOF1+t5sV!{~rrf#Iz61j#tRZA4%*cBYpAD!;Y45r#8YuQQPYt-LK54GrB zHCfit^0QIJCmyWdoLwI8>eVxHAM`oY&wR5F_q`IMgA1gt;rx`W-^(fZdA-udFHpS6 zHyp?3SY;B&Y~w4Ac~qXYj`jq5w$$`P z^1$`(EFpMx7`eRMB-O|qSC0AvPp{T1X{t7^OrO*g#)KSmtX@7%gB=XZ2I)j17u_ZA z3Eh^YYMcWWIU$yYjNX-KYd=>lC%XaXm$?LqhVJ|8YfF4+7s^nn?TV{VNl!+>LK!Ts zRjY{bkK*`rNR`B3e~Pr|KBeXJlkv!8m032W{5=l?zmn@mH)(EU6;W|v2Fgn}hf6n~rUgIC{oq{+;c)XBuzMek#-k17G`MYbgw zZ7%iVRV*n7^kuB6CPP@*PQNmL%3!eu57Ea=@>uH|6MKz2R9^O84Gei&iOoz3`K$;s zh8c=S58uOk;iHSVs2tA;PkdZw`1VcT@w*H5FgEu?-K0ZSJogFz7dkmH9=z<>T5hSqqrqyAHk%qqGwP2^mUH{ z^Xys3a7Dv~gjt+#Udf%9Y$PwiY~S577fXrdcmq?#m)<159%(oq zvsP(PkvU&u;W{U-`_Qdu!iMtU;9-ODev=*T;*)kXe>PnL{W(sQSC?~P--f)~5&Zr0 za4Fp7Z%@2-OcJ7em?uQ-kovP8?&YrEvIU8G;d3xKZh9383eJIqtCMGMyOk=PzAV3F z--*$@NbVuTxo2{3IGD7h|2x0d75$Ho8)@5fr04)NN8SDnY+jfyf)V9O?A*61R!H4L zD$8>BH1x)B1*ZA!y5q}*`1RArHQ0jw_WD=$BgAwLgxfp(!zYwGb_^dEa>mBYZLD4} zLk2O3VQ!N#9_biMqQn7 zQm}-NZ$V^wTyEpChDc6EEi@=x8~tWSuW)>1-dSq={myzi-fRgSouiF}5l3O6Iws9n zAKQx97;MfArO3KM3{OKwnYH<`S7XkRol?rC;zc&(AMJ3IYRewBC|?&Zw>O*HB#MT^ zzm!>YX+g!Z`3>L<~Lq%g-g^YtR=H$h9$NeHR^d-fEg=SNF>qGvX zTr4B?!ffoZi2QI@Yi^OtDt(2|Nor&^MJ};5wrb_yk)P)2Pm?nG*?qY|SHmcJ zpYrD6Ym)*Jk`XyoRJc!eb+?w$6ZM8*3MmaMJ6NAuoECakK1U$7aLL!nf44A4N9(0W z(@-QUDtARAKjCHL(u4BFXr|cljxNXCk8p!F=3P&H1GH9uu4W;CUHazG9O} zB#R1kd_!E^{dRDAN9)La?x^{K>mKTUx|$08!lZmnMtYmiyfXgpeFSb4yo`de>dPe- z&OW2A|sYogw&jBRYq^_1;^AV+(2ZY2LO^nR$xCUP~E%sVAwbU~LH&b`0I!`Eo|{ z!%kb6$<=D%Lq@u)``(Q3jgnN=%`+D@k0Ne7o$9Nr)q)!=Lpw`4tFmQ+ec!jKFyvu0 zdomB!v9PjS8d1;d_un%h)Zdn(Qab*%1bNf#WO1|(}j^R{pCQt)9{^!k3<*){Lj>FJyNmIjGd+*E~;z9NouLJ z&a-Qn6mGz{3-FO9=R~%S+A-V7OX?ral80RD{5sJYS*|16MsqNRfwmb((jD8UCNjA- zosWaUd$Kqeg_mV0LOh{bZ-@J@XgSR$RJ>nE{AkI(i{FhF7#*jY+|h|rLx{QDDvU_5 z8o;cambMHs?HIM7>k86)^GPMm(Gr?lf|ahSRvXeai@kpqY7li=^$4w{hw2K-n`uQe zp@wrPQVkuLc}2|NxNEm)YW*_2zs3}Fw?3gXb1d0&mbhR2nYQ+!Wf@JX=DEhYv_8c* zY0r!?=#+(0qVL}I#3STS1D9nthi~bR3t7lE^q!I&CfW73&5k#ki`|G}AZ!(Tn^^pQ zSF3(t!Yv~ah4ZRLcbk@D*)D3Y{3Hrq))xuGBh?s$u<=1fIng#f@85{sD#LxgK#UKbv!64}$j#I72#8nJ#mm zYD+(+Hj0l|*o(;Q@0|M-U7SoowI{s$*>Co~XNR$g2F4bo*Cq2jT*NvQC-L%iBKF+U zoo6V@ief3p=2JJQ>l}f)s)$e>F0=)X2H}Fq>IlwD`SO3v? zh5t9avNv~!1HA&#D-gW`(JK(W0?{iFy#moI5WND?D-gW`(JK(W0?{iFz54fLE}&OH zul`DU74jRSR~V!gKJIW+U%0E2sWsgEpGsgsesg^gB(Okz5Yz`jeGt?KL4EL#KL>&Y z7D!Gq5hSpF?;ZSeUk?lm7}j6OuzqWO5QFG{3=4q(6+$ro z3e3L(^RK}CD=`1+UpfEk*Z=eR5C9ecEC5(w{uP*i1?FFY`Bz~6)&KKxP~Cum0tSw}1EtfnEW<0(u4X z3g{KkE1*|Eum0hI;ZODfpjSYz{z`g9Up6+8v;Y7A literal 0 HcmV?d00001 diff --git a/proofs/solidity-verifier/fixtures/poseidon/algebra_fixtures.bin b/proofs/solidity-verifier/fixtures/poseidon/algebra_fixtures.bin new file mode 100644 index 0000000000000000000000000000000000000000..d2e1a2d30c3e29b437a1543ad3ad74b2e5b4a383 GIT binary patch literal 2272 zcmaKkX*?5*1AsToeNC>C7#8u0cZ%eiYgk6koDEw>HN;-`5ng0+zJ?B0a$6D^IfvX= zAya5J*PNlKT<@3v*Z2Q?e15;@F|EazZh&s1M2i^@u(abg)ts0cj9)emvG8%&x5_O$ z!2bshmeVkC@D6$Oq^f0jZtE1t#?AraMC+6cPq=V*CnZm+)@V#J!G0#S7iI zYlP-ZZv6aZsYdT4aB}y5?Sb;9xa8`QZG6GJC5x)JzeSQ3r+dGUU%`@!e9PN0Oy@Z~ zPmZkBfA#?k#$u(Vs{oTm9 z9M*X7OvFT)a^(~XH0?8!B7(Ypq!L2A7#JbjYuBiP)Z!Z*&}g&V&FAb7`|?}H+*2sP zUhI=A1EH`xbteiHZp>qdz;ye=jkSV>Z8QBM2ebJ3fi;pYiN3Tl8+2=`k~1WcKc3~* z6b1k-)E%NM_Cs|M@*EI|y^$DA!K5X`Ro0PDV>U|_KK7Wm$W!h>-1V7>^ZnJM2_QV% z|5jdcPks++r6M=(Rg>X(@#n|1Llw6Bqr$uVmL;_c*LDA@sO#KhmIaQ0oy|wMG#W$} zq^l^Xi7`PKe^`5LyLrl?(C-@mPYR#;j+V!jsiSWZU;Cu)Uppu=HBBy@=Po^S?}83v zGZT)@E}8>~t(Wr=126M~&(4rL(!F>Qwux$1t$+CbQoN&{A(PwpTG-oxZ8|DRO*3Un zOjKp;@0FihHfo#zeb-22B9i<{<<7tv7dPtMN z`Kvr0aZ{HygASI80I01k7&b>tP5Hs3wO??eYK7v8popJgAd2NwVy7^VnSQR*vZSjD zX!iI@nctHM+ANCJ6?V=(>}EHS+o;-VGv-_g3=MLxuPAGcEieFeTfXc0DF!O5uph=? zDG8HkSGStgeU_{0&YZu~FH>Jx1D61X+cyV+%pHg4*=Vf+U4TUZJ*Oo`waCl=$ELPR zsXP?ia9X{??x=lrFiFB~9kF7sQ4{gn;Z#x?G&j;Af!+F2xp|! zBqto^7@buz##|Ke(@X_)Upsz>SCS6#xjVC$;2}$1IJ=dF@n) zP&x)Sti<-07bN+y#9g-Q%TM!G*bZem8_hF*&vd2hDA*`E56KjRoHEQ-eXm>_Z0{VD zCd5?FF~cb5o(%05?$?B-Vexr7R)BAV7+aFpalPy=PITM4fYXK$$E|Ll{bzN`gK(&X zBG99uAPtU%*$S_QJT6_WbVcFCAOtkClbdhP1z+hPub zTKcCE;(0L7Hf?eq<+1b6xz7t~uwkh)tn4PfjY}I_*to6j!n=^3=_F{O1?{YnP*bL@ zCEQuJbIbaufqL;}^PaZBsPmHTrgDWcoumeuZ+pi zT^}|J`aoMqObd$I99^Mv3Eh7I5+j{8{0)GIONHc`LVqmaeY*-8pZ9B<1L4hPF{Rt7 zwob?DF=<1KM#LYjB`=3_B<4SNeSzpjXW3jFr+4ruI#XOd(|z)ZF-*&wL;S-HY3^Z0 z3&7dEcI&K*R&6}32--VCHe+N zh>AgW&RS%|BQ4alq-OSOdFJzYQjl?1?-;3oE>|5Z#e2UIDsGM_d#xBWKpP+|dwrlq zTLs{!Fh}j(MJK2#aX0r2QdcD_h2!R1J<5}Ns~wyAe{kopU}(!S(ykpHTN@tJ>NwSj zl?zrIn@zj4v;JNB^>kFC*vZwXI zt#`W>+vb@%4aN) z0P zufk^qq5$@iTA#VapUyuZIVDz;&2N6tsm*(hDnj3t|A>ytdla}l!$v%ZY8@F2-H;O~ zLdD<)Q6rp8(hE~{%U^kYnVEvQ zpI4g_FFG0Gc+qLkfrqJ*VG#OKYPYmhX9M9OM zCz*NPo!g@OvzTQECD_bXwDz)*{(48vmlnwB z;j|3%+eX^XFPmn%fc@8J}eJt(URxMSFFj-1}U3suCu8#V(= z_Ph&*X_;%)jKYz9IW)T@EMh2PQHAdo<5>lAi5pV19I!g`$W|{VJs|Cl-c~U1egD91 zim2UXpp>iVnCxP>xFGi1gkojWK#0|#^rb9O5zg-d#sytCY-CqdTKmk)cjE8PSeqCt zaKgd>`k{^1@iONnlyiGj1;u2>rN|}9II|WbGIHKqstQ!!v8V0TOyfOjQZOd2DQ;IO-=6cU%7_=a7K^+^pc&d3;nF@#iiFon- zb~B&>aSHWwZ2r>tc-4ZB3+5TTEs^bd^5^S=saG#>ePg&SaaxCE^VAvt-`|?fx*e}|EFie1CCO@gvv1gshnI6& zoYpOxBaG%;1~Cft3!l<&HJFioP@wtNw5Mf8YV2pTZ#RABIPKKl{c6$I1)r}T)5$4k zyxRAuK;c2(oE_UIAGP=qvzSwg(Q4aD=fhLZ-}VY^h}E$AX1x0H0sE)|zGwWCWRmY+ z$`InO5|%5J zr4~{#U%`aIVj+iVN>?w3Yinul?r`RxM?Z!wv-z^`_zolMl!Ak2Sp<{|&B~-~gI=Fm zUGQV+nI8U%kj~&;=3n3HF?pM)w88Yc*IU?(`_1>e UFXjv66x!I##MSXiJ#KO+0Q#yJvj6}9 literal 0 HcmV?d00001 diff --git a/proofs/solidity-verifier/fixtures/poseidon/evals_signature_fixture.bin b/proofs/solidity-verifier/fixtures/poseidon/evals_signature_fixture.bin new file mode 100644 index 0000000000000000000000000000000000000000..38b28ab00d270c89fef7688860c7a34110562531 GIT binary patch literal 64 ucmZQzzzg(k7X~t8ar5x<@e2qt;026d@JcfPZE$nML_;E0r@io|BL?j|Nnmw{!74AK}H6Cpc{d{1RBT+GLV3VBLw6j z1~LMJ3uqu45e6P5%0MPyxC0GjC&IvE1Pl~qWDo>;8G@96F$D}v0^a6#yh)UyjNk}j z1Q`mB9K4$NcbMXpXJCN16qxQn2xKS+L6-^`ydhxdbfEVkiB&<7LAp`oaN|y;zp=S0 zLIRI&EbF>j;t;%RUU{O#JxBJfQsE;PD?`|zO+8OKMan8Ks*He4#A{DhQ-0|}Be|{v&Tfn>rOmhql zzKZOYtrz{AwY;w9+OK0fRbuEpmH+w1>t7kq8m`z}g6b`9of8F4DaZCU?n&aC7v$?* z{Bo0FS*4Ty_g`YnQ^PzzqI!$}ewy#nfXH7_lRJ(%GRJ!6a&MJvcJ|ovXvLqNyusI zu*Ug{EPPa$a^vq_-D&mSkA4P*Hac7|`mpj{FaiA>Id5-d9C*H0O4a#e+ULhJ|4-}| zYG6+7y(6DL%cJ?(=UgqDz5CnRxsIRLzqHD>Sm9hlVs>ed(UuU&sTOkm+Oz5%&V}67 hITwA;+QRy}XFu;Aw{Qs^<(>_T)feAY-&Hn!0svU-Q~Lk_ literal 0 HcmV?d00001 diff --git a/proofs/solidity-verifier/fixtures/poseidon/pairing_fixture.bin b/proofs/solidity-verifier/fixtures/poseidon/pairing_fixture.bin new file mode 100644 index 000000000..548c4043f --- /dev/null +++ b/proofs/solidity-verifier/fixtures/poseidon/pairing_fixture.bin @@ -0,0 +1 @@ +ƒýÚß)}﫦%qJ`-WQ5qyA1Zf5sLOP`dgaIUn?(VLk8w8}gOF}|Qx`q;ADCwM`V`Mx( z;XIsmAMSqL>sxED?`4M{5Ut+@?Z*}by=hyN6~bCuU_AXzci~AxJKMv%Y!v+G1Z?f@ z`v+qp@+GI<+vKI>1caQA`Nhk8@iDpXoQ8XR4jyk20j2pk;#nOMYS&G~ULkmq^dnX& zrZsoAv4cPx@ugWtQ4s@8J@+kl7-*d2mH*K*L4jcuR=3F+H@cRBCeWOGma|SZ&}67K zlS-sLHGyS3JZ}mYv7Dw|MV2B-z$1~=+NRXRd6aij-;1Li>I@I z^^y*DcFjG>I?H0*4H}o*2z7evTN(2BaEhs!sK1`nJVPJn%wd~)ino;%UmEL#qS{wR z#g;8LJ87f#3+6w9XN-rM@mhZPhiKSY)}!e*uV^uOOZ6@kCTK=`dS;dIB3`c|m({+Q z#3cw$d;Kn-3Co!D-i<_ic*$iFt^p-LJt-1mZZf{K+TsnLS<6f&zFLKGPfc+#J1BHm zv>7i)(9CK>LlO$uovGh8AV~MeWgeM))7cRvz<1rU^z)h13ky>4eyeB#U{&_U%Vk`v7>0-=|>*kyF z^@62fK$`VfP2=NXSwR}8s&w2-k1wiyII~{ACP`>vFWAmHMArT1l10fx=wbl@UMU|L z4{5UcpYpgdfmnOjD><Y`K#9~>Xt$Kaju^|Y7}skhCHyi}rZE%$ z`%@Zf40GXz?SLU1v?RyA1DR&!!VjQ#hba+@a-6PcfMr{CFn*%?nB4ct$NmbMeJfwg zq#)z#qqjuqEhuv2`=)Yb%m)}#sLm2V53rQ?_>x`zWBv>mJd?S#ILBD85z?_Pg-hLEFjaskp3u@a z#fdawK%&_jM#WP1r&stAFhq;^{X?v&m0=;H{=S82r?iS2m4*#P?D$P^y-t)IAe8cB zcuxR4QQ2zvamPd_=aEa9!P=Sxf6}<)RyO(}CHY{|R{di`R2L=yu>ZWP2Ew84-=6gE zwpq-f7V_^_3X&#McZd4Z+!;@Mgph8?XU2Qnyrx%7AMcM7qV{MXuQlIMhEwJYeWgv-1ifHmxXO@;!Y4$(y^|bg{(dw4Bf-Xj zK=mV7r|~c8*UJ=}_;3vV+VAA@gA*mRf8f=UJr&#yJZS$V>d9nEH6a@F60K+^D{@Ss z*@+)2UnEsN;%^Tq*FF?c7M`_B5KuW5=|df@wLDu3QqA5i5ciGbutDJzPCpCLU%U5v z*an2S?J&T>QKZ%`uLb)h0Lqa!cM#RVU&eLJs&7WUlb`Ev^&WTe;ml`m8Ew3?Z>TT_ zM>2-^t5WBb|I57p^Ew=>g%q1~;9ik7d2ZZU>P0GaApNZPJgkr*VFY~IXBZsmN?Y;5 zM}s;3Hp3CYkey(6E%A&o;=g8h+ixhWt<-;3qUd;p`whs=Z2k|~6pFM+wuwA3?OA{|x~SK#9_V1+w?B z|4Ou86qWsbS??bhD%KGYjqQJ`xvWJNx<6DP1a6EXlb2hhoL%E8QvcxN+k=v(?B&Y$&(35>DYC#E z<rQXPzGRUTVrk18-HEpg^w4&7oN&u3#!of!LhSEt@gdzQ zA|+4iP>~Wo41(n{m|VNovC%)p@MvKwso_E7Iq_pk+Yh(UxD1t5Hl>?`jwJ&n3m{&w zjq5}p0rNe*g7c8`k5WDn?&!G}j34ZDu7M)yJ@nF^LO%`!!oKzV9lay`h-t{1AFEQb zbhoA00)k?D9^i&K&tD5KMy&tqEkP$7wN$6_7BeBSv=g zr?{9r=MIj|n-V-2su7RS5OV(hI8jBCSqxL;==J*Yf!?4^v?dVJ#m9AOx?k8%X*dLn zSgjB_=&a!dJ*B*2wl^Y0m)Y0vJa-u4Q+$_|C4R%^nW2)M-kS->cy@BR*F&C&4zG@V zT;K_rZgc9ovK~(RVhQ<@Uk~9g*lBrp%Aa6+nOT?$|;!Gm+yP zNEJ`|i0oyY(R#vOy~M3&{_eoD5J-vqZGc1ShBb)3+3-j`Tk_ zist7n?5)$7&?eD|KwU^MmDr7ZX2Y=<8$Wcxb;wM~=C5U>(eSY=JHMujvT><(#5=$^ zQPe-&IS*ItOibY$W;B^$_cu7K5fY1#jW&fBbBB&}3OCml1Jx$yZ})R?SF31gUF^k; zIm*k`F{tOq*mgwcL2Kr4Mb;f6a1IP&-W$l?h_Oa`MG#voo_D42iH7yRIrQ`xS#O_U1`w%`G3ZM~@I?-TMUd7gZ#1Dp`9 zDsr?h|BIJ`v$4ysy2S}DJ_xY5g@b^nyJ8o6p||eeNsX-#tkMsC(lb9#8E{@h2=22Z zxgw1AZkS*)2!^JPRU4}O*Mttm`Wxsh453$wCJqXDm!lLx2UkoPo8NE70V{M>m6PS> zOZxj$K?7VYv#r8x?1?s|(H^iX3y}l?mUenR?v$v}B&Kp5sF=JpRnla69OGxd%9zkT z&96zy_1lFsB3z|?Gvc|@5Yd7GXFSbgGFhcR;57-2{EoOSw)P(F(zo)UJ2? zO?9g;_dz#))|qpDL*7(8xjDC-y6iIPrK$p1EZung5$Ak=L&i-M?JW4D`92YuKk;NG z*6!Hn6UxFvUA_@j4s}f~pK4!wtzCCMa!JA8=c=jDZ7j++<>mJJuPfvIuLpQgmIVQ^ z8px%Vl^WI}hKu0Vn1rTl%D*Zxe7_GW{M9ZZIh+{aD!(*ad8%WP+Rcc$FJ#XMktk&Z zZu4x0>r(zOoasflq2aa6>Rtu3`W+L@Z{)P*_&L}z@l-~Al{EU@O59M#+8r+J5_xv| z`=0vAQAe23!+K*RFIUy;Vnz5>RkrHUy}O!s1vjp4BMP-!_KIcdbW3+vwnlL5{Dcx+ z9z=a1^I;zov%tL_9@C2WL2t7cu|?PDcLhv*@8o*YN+<&H-$m6Pk6x>S?Y?z5ExuzB zjs^wh$^i+0_BxvVk4{Xz%e*w#(!<2_!@~D9+r!SUT>umJ?c6dLT4evLzyI%iGoycs zZAjCnen~V(QYYY|n;x=TMQKU7$perI3G4f`h5#@!FsRImPG-W^(LQkbt_-o+_E2aXv)sip= zi*yNwTJ>}j;i%L0%A4Cqll?=MaW-n?4Vn6RHU(7#EK>I)Wbb=N_07(uU7YLf;Du#I z5SEZqiV=Q+4Y}ZC^t^$dLCM1;!PZ=68?#Xc+oAe*(Wh-c3DGC`gVi0pvoVnj17;(SjZI?2%~aYC zg-whKuRz>ocreokLI)R}gp-Ud^^yCGDUX)G0c2i*$E=R=8i;ndK^W=X}+N1mnQzyUHA@f2q`cJrCdDKg{awxx-*x@rdUs4 z)Gte0g_-rwS<()M>3^G1$#Jm)7FS{Bs;V_d46b4!?jsE$cMTtr+WPRpu#%M?BqMyf zh&Inv>L}RYWCcRjv$4mbxH5Yt*PR5m)|l*X{gq?znw*im*I#q&WQeO#^+*N9qCV0 z#>vSF7qR){18Xz!l^Nue7s6-10$1Pr^qZJ!pdX`+y5~Pc1MDKPQ=h-4Vy$hx4Te+H nRZ6;a-gyE&*6`I`7GnH{WzqAlYBlKWGs{Z?{n)Y7t-SvO1%qBq literal 0 HcmV?d00001 diff --git a/proofs/solidity-verifier/fixtures/poseidon/query_schedule_fixture.bin b/proofs/solidity-verifier/fixtures/poseidon/query_schedule_fixture.bin new file mode 100644 index 0000000000000000000000000000000000000000..a74e0ea2fa1afdfebe87ff3f539164c2e7e6bf96 GIT binary patch literal 296 zcmZQzzz5_K@yP<^m9{62qxt9+j^kW*hyJuIoD%xQ;OxXNZx2fa zd^P<4|Nr;YYUW8l>fe<%A8CzHXt2pYSnd63;j>cJc=O!V0RpxL1nlAl%QG@E5>N+} N<|nKKSp^dV0|1tkC0YOg literal 0 HcmV?d00001 diff --git a/proofs/solidity-verifier/fixtures/poseidon/right_g1_fixture.bin b/proofs/solidity-verifier/fixtures/poseidon/right_g1_fixture.bin new file mode 100644 index 0000000000000000000000000000000000000000..e634a53d82edab00c04f4dd9b6cc5c557c5e07e3 GIT binary patch literal 160 zcmZQzKm~ld?Hoac9fwQRSKHiHm^5R~_s9D=7NxyWs${QT*t5LZ;OK$6fcPB;-b%jN zzjWyf6I9&{3_Qtg!9k2Ob7p?l;Paf~>)yU~!@{Lk|C?&xx%6wLW`wdqRNL-72R#`# t?Ra%-YRlDma!UVmo$S_R81kfR+>5+)x=KLt?0XM}R}Iku@(+{3bpdBKK3f0) literal 0 HcmV?d00001 diff --git a/proofs/solidity-verifier/fixtures/poseidon/right_msm_inputs_digest_fixture.bin b/proofs/solidity-verifier/fixtures/poseidon/right_msm_inputs_digest_fixture.bin new file mode 100644 index 0000000000000000000000000000000000000000..4694a6694a1c5bdc5188d796a83f6b2ded7c1d92 GIT binary patch literal 40 scmeyE{7A-m>JtvxjmjL{p}QHQy_%esZ~j>RvF^j=S3*pl3=m)l0Ak4x0RR91 literal 0 HcmV?d00001 diff --git a/proofs/solidity-verifier/fixtures/poseidon/right_msm_terms_fixture.bin b/proofs/solidity-verifier/fixtures/poseidon/right_msm_terms_fixture.bin new file mode 100644 index 0000000000000000000000000000000000000000..149c3f4adcc453c297ace927e242cf287726d93f GIT binary patch literal 8334 zcmc(kWmuG3yT^y_&S5}m)7=aW(y2%{2#84Mh|(Q`64D(Kf^>rjIMN*=2qK_#N=h7f z&-QG6=Dp6>eSeyD-PeEpf6uexUh5eU2*d{hfiNwty&m4TG3UMh{r?3LFp2aHclPM% zN_fW8ziP&11yDbY(QeE@w%x&0LoM8shQ43dZLD3rzasYr6SL~D#ZCbA$T8?nBf26M z^BgQAy~>e{Jnxzg?(iE5Z|`+3P=fh_Vk3E`H)J~Kym8KP2TsNcuJylIZ@B*mjz{VD z^>^q>aNpLHvn5n$AV zPx5&60z1PX1hvUzlLJXhUK46TI>cJ0^2DUGG?79Ws3*cL{~*0YQnX2H@v1$B$STCD zT~Y3~^ZqNOp(n{frR}eFaKsB!GusW=C1O9Kna=*kDgf*&D^LC%$<$uW-bBF*B&DJ) zV4n1OG(%5dT0h3_U6j1mh=G7Ydg1cla0jl6(* z{6RzVmCY=EKc-Y6b^RGZko5Ty$KspN9ZmU)C?st=m6L$q<(Hpsydv=WIsvuu`Po?SwC* zGsYf{hFx-)zd(zj^c$-XurKEf!6(7~w;j%H7xV%cChdTEWNEQ@1{J=2sd3;KLQcEW5t(6|G0%Ip%*dje<5eh~f}E5dwQLpxX4yF)?~eKj&((WQnk zhUjFe6=eb;wNr4>Nz3^Ls*sh8k$X0S#+{XzRc{~rY1mo425DDLY0-H~wvJ^*YXVvE z`n8EE-nlSFc^6O8+A2|fKCLa`GIFwzti(xvIM;Y@!eQ-r+D}2jD3E@U!mhIXH&%po zjz_~%+31nQ&oo%IeC|440`x+2cTdXY-lh5Yb5b?cBqExBz+r|9CTER!aXNMe-L`AL%#o>w=uzhSF_+a{l}v^nf)k9 zUC@eUcc_j8`Os3#U)OphE?_4pPNU(phDjCq_4`&2$+H`Pd9WjTu2Bw2){ItbHAuAR4kGYXgW zE4BrvFE7aTZ>B2KWOhs)uq*rH!ox;GV&dpP&c@)zm~&kc_zeewZacDJ0B%t!$)HSI1X!4ho(P z*EC-1lH~`Fsz?L%c;WI?xFXnjZ}Ap2>dV3=@Umod>Y0bD_+ZZiQRv z6F`>3S!Ee+mNDXRH)V^nOpCqepP7-i5B9Sj1DYTWeoBnR+y6Mn(cjNbZ!ceOSz|mI z1RlCG>KC2C>`2L0zDuyuBXdgU9nZX*3*`jr(KYWMWL(%|IFm=Vbsf16!ujoY87g{A z_edaJjWVBFA~&HVWt_*YYji2u?gYrE{}uAsZ#Q^&FMn#>q7xg;yhaE#!6bG|RcS*X z@|f9zeLNg1LL>|c7xM9lyGQ#SoYa1n#(@GwT|Ssf2I{dzOo;As-Pdcb zj_>R)+J+3O_3})s2I=vWGCp5-?IAQX+{U0MtD@vIhSo2z2#EeyNPZB1_0NO|t5)c7 zf&h`?O*Qxfpz5CeuJShdW+@# z5o&UtYcb+*cNRB!itWS9!d{vGs;Z&8($^qUB6}3)JIf zbfQ?_diKJSE2HR^$-t&9gm#bmS>kTGsTl^m$!ENFFg z6iB=zbs^IVz`qodRr~5|jusV)OTb8;prod=Wb%_y2dGD05t1jOF?lA364U#EOX9~^ zQ;w5Xg(e0?tdUNEM&F{8Xg*zs^}x@cM}wchHmq*{pdyrZv6n4)DwpAG&7S?BDTN+{ zD*!mKzG5C=o;Ab+>Yvwl1Gxmm+C%-LUcs8{y{~k z?CU~Xg1Wh9nO@jjR;sf4H}&9;z}cS&e!f#gQE8~fL0|Hr%);P)Z#@Mje5LJeuZTPi z=eSb#BpI*4S91cpTTfV}5K09xX0=pBm!k<9M+LEJhR-{)Mm(P5Hqot8i3PIagW^vM zwGrE_Wgj7I$Boq`4oLRb=6}!Co4x9fmav*zsZJNKT3Scx?R zYv`RUjyr_NRpe!m0H2Pek0+Ej<1Cy!e?h1|Q7^9bop$g-5UF9p)TN!Z{th zktSomy3l=z&1k$i3jK|Wa0TmwJyi)qP>-5*BKwJ7&)Izr+WdS})G?`- z!wt1i1|r`8C`|Fs3f&F2FhGc3I}2cU^NiKYH&^_IB=@lK4judOw^u)30G4ZRIf=W6#^h-zc2;-brZe8_pFl-P(rT zLAhUnop&c>fX=L6crxVZT8#O>)rt>cvUbYo9?kW9OmFn!;F->EJ^RDx-)a0}MVPI6 zpqUR$c5^i~%yH@Sic$`{7nZcxZDN{f}?a`>Dtq zr(Q0}N1SPlZvPI8u)1crjrBFdx|@;?;TvN3bQ(Zd>}N|1Rdx!!5ETV+!JKEPb}(}8 z<4`7HW`=qS>yr)1CjxIW1>#3z8OjsSZT6}WMh?BA)TiyPUhW(9=q-=>6l_JG;GRGf zF~Kikyha}-Ekc}IH2A(!rc)NCkgl`6ZEXD=7GZhWTJZ*%9F}Mq7eOX1;`h*iu*jZt zC|&bp;51#jH+xRH`LP_ekFdgZjRkXiG@Z|zhK;rQ3Ljo1P@sY7>Gm@T0(K^ln=wVB{UtGm^>tQD3E>JbkMlq% z@w}J9H2z6*f|%NxpT06EY($UuX$8Z%Q%c?8%0g68-*HL&-R1h7@GLsq`#?Qq>@jbg zf@RZc^&zgWt?EmGz0-L#l*ZA7jeI>yUHAE@);_XiJJKfv8{*9cxMq;wVG*`j=a8dD z^#U5&6p7C?^u7^EKr`^2XyuFQWV0$tIfy+N-NXLvwn(0yCh3Dt;mJUc32ZDZP+Pwx zO6818hAvp@jTcal@_m{f`6DVr^3Zb9wr{=oQMXB3lfKg22~bb!Jv+A|MDLO< zEL*dMROvPnni71XZd!6FVY7)7V@vD99v4%XUc#>(WFY>*&;2Lee#4j2AavIY- zev-}3CtkLd1E_1EyLygeIgw}5$MIc^4f|@OxUba$^NXB= zP4|&#w7N5f#k$w`SW>88JRK&%AoQD1ekSsYX@Sk)H&#LSIcnz|`!7fzvZYg^X|xnW zt5k!{=2K*Yk17=krAhOFUjIV-*LxLGe9Rv<62ItOaAL8lN>(%7dd4Wz%!GZuW%o63 z_R*C$kd3hPNJSxDrKjxNJQtE_#+JvO@MSUqTHe+>P?oY`$^N5iu$x3^e%5=;oi)X5 z^a)T;;2=9-Q{nxF?K>rR04U*3OJZ=-DAh;m(n)DQZ3k&qd{wE%a$+Bkwzq*}wBrI@ z*ZNF$&rW$94Fn`3vw7BOFpl@E7ul)Ag#?U#+1; zx=Co_F5*QMjIH@$z013m|3KI2#Qx=H&Bdc?9kEphlYrGaAS=?&ezH}S0$NROC%pFS z-Sn|gkNg8u18jN9@6MS7g55MFKILALc(;OHX=jgz!>zCNh@VzLcg9W3*60T4QhW$dy(_tuCzXdacr#o z_RYUue*}7_t7%KsNr^DghWiU&_PiRR@@BTwdp|m`nA-vPfzBr5ccji3Pb&V9aRo_dS7n9Ia02%Ag;6Rn%QEHUa{m?(f!Y2{`%W z$}6f~&yx7SIBEvb+sE(D)R&~7Ss_t3CvjvhB}fBJ+yV`O_la$4>^co&y)kRZ(P zu{3)n>$!mfxyBMuxk0!KSO)+T-$ZG=!-BsyRAF7?7 zV!PP04Dc49o=AhnPIpqFxYXCHXPBFQ|3RSr4ow|KudQy=WZ!E!VNGYzX8e9nNV(43 KR1>tb?|%UKk`ERD literal 0 HcmV?d00001 diff --git a/proofs/solidity-verifier/fixtures/poseidon/rust_trace.json b/proofs/solidity-verifier/fixtures/poseidon/rust_trace.json new file mode 100644 index 000000000..5179d007f --- /dev/null +++ b/proofs/solidity-verifier/fixtures/poseidon/rust_trace.json @@ -0,0 +1,1047 @@ +{ + "entries": [ + { + "kind": "Intermediate", + "data": { + "tag": "vk_repr", + "fe_be_hex": "4cc2e5ac0d3a524691c3723a18e826d6ed01cbf767fdec83fcf3a8f9e7640494" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[0]", + "eip2537_hex": "81e60cdbf67ce5879c7c24b3d3413304dad21febd91bee73191acebb2ed5597de2ea65647177e202c983122854298ed4" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[1]", + "eip2537_hex": "8e8bf0427660ee129b3ac742b997adf709272d75363146d0880dce510e186fdef0e49e65d30f5304a203642b965b6a31" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[2]", + "eip2537_hex": "b30df15f939f9f1e19ac2bf52b805ac80e1379e8fd0a9c588404b95cec2b014f444e396014ce28ab497b5cc2aa941736" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[3]", + "eip2537_hex": "b4908b22c8819acb07f0d59150a7118f3c0a2d3b97bb0b16175fa2ac34020077553e5a94a5a99d5809c03435e9aa5a76" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[4]", + "eip2537_hex": "83fd77a149152709931d80ffc6e618a834fe0110d53926df32c8728290432217817fd8dca035511a09d4a52844f9c492" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[5]", + "eip2537_hex": "a23f3a922265548de6e1e3607311b7a3867156aed5b30b51bcacb21e9a2f06a01820189deaa9fdd1f9cf60dcde5445f5" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[6]", + "eip2537_hex": "a61599aeab3384b4d6c638d561deb71a84e59cd05365cd5bc2b008b1bf797e4d6862ac0052b0d64f20753dbcd29e8b4e" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "advice[7]", + "eip2537_hex": "85737395a12ef01dd8fbd54a995c888b32cc74d9a4cd8093ca75e38300f8122a5c814dd10afefa450b03de1f1263df2e" + } + }, + { + "kind": "Challenge", + "data": { + "name": "theta", + "fe_be_hex": "5da79f088ec824cf983df50d2c7024ee2a6fc2d14e245e3f4de569f1c34325a2" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "lookup_mult", + "eip2537_hex": "81cdda948e0df2d8dc2bcbcb2a216a44b661b35bd58219ce50d17e8b9c266d1829adf00fe5c840f920779226f0a1392c" + } + }, + { + "kind": "Challenge", + "data": { + "name": "beta", + "fe_be_hex": "0477a5891ccb8eb3a708632c708a2b7bb5f059dc95e1bc25f44fb417ffa65c4c" + } + }, + { + "kind": "Challenge", + "data": { + "name": "gamma", + "fe_be_hex": "6bd3cf7318992ee1dcb6e5e5f7bd78e122a1453318c7087d3ac00045ebd1aab8" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "perm_product", + "eip2537_hex": "b9411cd094ccd2d23defbe63850a39a796ddcdcef44f7fe1241bb6505d56fc4178f4ecbb5ec6ba5014dc4ef11bc9517a" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "perm_product", + "eip2537_hex": "8da59ef4d22efa935ea36a228d6f71008adee8ff0b230d2538320b4d467b35a6f6e909b6c781bedee2dc9b7e744855cf" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "perm_product", + "eip2537_hex": "a45f4dd167ca3437171ad6d4b982403b44f2866132141b865fe9abd0b055547d3ddc3919db874e5be9c3413219d1493f" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "lookup_helper", + "eip2537_hex": "8812729949bd06ce74d7ca0e1adc1025ecb636db79cf8e3711361b87357a748f531fe71995befa9a2b033904e4f3d697" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "lookup_acc", + "eip2537_hex": "87f8ff32948633aac0ff020e896770d4774cf90fa519fba2e8b50c0342e0effefd9c4c37523589cf84f9d8e399afe3db" + } + }, + { + "kind": "Challenge", + "data": { + "name": "trash_challenge", + "fe_be_hex": "3a4f48dfa840a95f19d2812de2e376cb6d2721edf593f257457f133d5a0dd195" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "trashcan", + "eip2537_hex": "a3b09d6ea5473d836e08e82f77870a56cea906cd667632771fb78b207a074a0098782a87ef02b15cfa7266f159e6135f" + } + }, + { + "kind": "Challenge", + "data": { + "name": "y", + "fe_be_hex": "61e192f45558aa4d0fc17f09a9f2b2ba0b9fe983c07f0eabf714aa039c4e4784" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "quotient_limb", + "eip2537_hex": "aa755f4c64bde20de81c6e98257562e8aaf8bc4bfb60d51fad0f418eecce3ca00d5d95d77abe72a8727f38be78584ee4" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "quotient_limb", + "eip2537_hex": "b0d93216a83f47e689512965c5848f8c39b67eb858c414fe6427a491ef7f7cfef561d566caeaa86bc45cbdb95747dcf8" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "quotient_limb", + "eip2537_hex": "aaabe8dd6047da8923e93cb0f643f933a6acc0b2bde278eca4ee4ed8063431a13d9dd1b93d3be13822d7866ff3464609" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "quotient_limb", + "eip2537_hex": "a3fe945b20303eeb1918026034ade07ac206008d6bbde640b0479e4b5a2ce78f82d342286f0035d5674c65098c4cc742" + } + }, + { + "kind": "Challenge", + "data": { + "name": "x", + "fe_be_hex": "516a5fbb000a58f7f6b39658291a583eeee4deb9452ccfa76e0e6bc648517789" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[0]", + "fe_be_hex": "517f20dcf80347a4af3efb14feb1920c29e8f35b7c8d037763e519a5bef9caba" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[1]", + "fe_be_hex": "3b6703300fd2e08ed62718bbcd69dd0e7f3314109b53432612f99ad4355435d4" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[2]", + "fe_be_hex": "6b88dacf5c9ade93ba2cc6dcb43b48daa909dabc5650fe3aed07e7c6031aa241" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[3]", + "fe_be_hex": "13fa71a2451f245f8c353491c945b5d43308373549dcd752f144b8c70fc47487" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[4]", + "fe_be_hex": "51ac7df4c88715664d1770483fb75d61e5551f9e58635d87f8f73ab1f0dc3b09" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[5]", + "fe_be_hex": "41f5b65bca090e64641e472af997515ce1f9c363478cef7abb8153167f354284" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[6]", + "fe_be_hex": "331897d366b979cae0be18065a4ae3fde5353503b884ad534c67cae68e8ff885" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[7]", + "fe_be_hex": "1a3388bec892451c1df4728a9443dd0f7e72cc5bf61979d1714c113f7c46b979" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[8]", + "fe_be_hex": "526f8b9718f6240618d06483776b09f7a746a2435f5a2ddee65abefe4046dc78" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[9]", + "fe_be_hex": "06a3c35c61e9d8ee13cc5b41cf1ba0a246eb65e9c5a9667a30c6be5a2572d4c6" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "advice_eval[10]", + "fe_be_hex": "0563630d40928737c052e61519b691f8426b82661400178b46aea061e4ba71b6" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[0]", + "fe_be_hex": "268a9fbd4efbd107a8e7010dd2eed90b5a3190acdb4ce1754970ccaa1ca5e367" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[1]", + "fe_be_hex": "700063fb2571a376cdc61ff188811118508dfb4302372e35ab85b993cf510b0f" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[2]", + "fe_be_hex": "406b9bb2bd3c47a83f4edd3b877a4adf682366966d11a734a03b2972c7adcecc" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[3]", + "fe_be_hex": "31e6dd0a5392def7d00614b7b4557e76152058a7c3dd9c015c41b3d601ec4028" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[4]", + "fe_be_hex": "2dad2ba5bb3ca8efe21aa1c9c14314baaa3002818a668ff19d0ffcae5165f9a8" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[5]", + "fe_be_hex": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[6]", + "fe_be_hex": "169ff8aa47c0e009f9a68d10a64b8c967f2042d65c00b23bd04d7ea946118e4c" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[7]", + "fe_be_hex": "019ec56c15efd12597c4bd0dd22c5f909cb2734f646bd1559f6b170a3bb4ec9e" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[8]", + "fe_be_hex": "21ab5bd7c14989f6f48347353bbe321d6e640f8465ef1ee1707e7a6772be711d" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[9]", + "fe_be_hex": "47cf90a730c293c4c10317f49675661f29ec21ef09e3f954f5acfd8e75c62449" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[10]", + "fe_be_hex": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[11]", + "fe_be_hex": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[12]", + "fe_be_hex": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[13]", + "fe_be_hex": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "fixed_eval[14]", + "fe_be_hex": "58bdeb65c48b1e9e80eda0ec92c0d190103d6dc8cf8e1a45f7c00f050fb10481" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[0]", + "fe_be_hex": "46503458bef568da79c402fd3bf3688b961ef06b93f58a214d7610a51a6f837d" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[1]", + "fe_be_hex": "0a4ba78937bd1619696ced83668e61fb7faffbff60212b39b507e86b45a1ecbe" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[2]", + "fe_be_hex": "35e914b41778d1011fa5a36aaae152f5fe4e9894b83c6e284f2bf066bdf839a7" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[3]", + "fe_be_hex": "5de3c6628398eb56b4ee98ebb90fec16819bc7d5cf56d032ec2735904af6ef10" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[4]", + "fe_be_hex": "651a37fe7a65f0b59b56f279ca745b55c6c1d1e0d8e8b2ca0c0849697b5052e0" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[5]", + "fe_be_hex": "50ed18e2e66005d348f47905857ab6387f7b79ac12fca39feeb23bfc05e49631" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[6]", + "fe_be_hex": "515268a1896100bb6dc0dbf63f066eb0f6eb2cdf303aa569aa1ce6d1801f66b2" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_common[7]", + "fe_be_hex": "163ffebb77764b4c9f2adace16d342431184ae65339cc2e57f114fd53c9e26d2" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_cur", + "fe_be_hex": "05ae92b954d278df54710b903fb36301eb584e2fc603619f8f1e9496799b2abc" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_next", + "fe_be_hex": "1571fb2fdfe4e5f074599009a09570c96b6f50727b688ae1c69ab322334222f1" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_last", + "fe_be_hex": "46c78a97147c819048dc1a0838c5858764e32a0340a4321e763b83feabea3c83" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_cur", + "fe_be_hex": "6dc26d441cf7210a7bc96f667d0839614656d4b6e6f44625d8489388d1f5f8e0" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_next", + "fe_be_hex": "530bf6c6fcbbb58031e6bf33733f1cbb923639f351684b1f10cf852b362fa2bf" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_last", + "fe_be_hex": "4d0f0489bf7cda84d334f3d06d8007e67305d15ab145dff6d4a148879b2e5803" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_cur", + "fe_be_hex": "57c6f9f7d2f53cfed65ddcedc777c41aea0836882214ce362dec8b98f874594d" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "perm_next", + "fe_be_hex": "230340fc9bff6f0403cb5d9623a91bf19422ad38b271bc9aa76aa54c7e48c52a" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "lookup_m_eval", + "fe_be_hex": "3859841e32e17933d3d6376476cbc9fec6fdac6dd1770c5c56682dd38b1bcb81" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "lookup_helper_eval", + "fe_be_hex": "51cf8b7af274258426f9baf4c7b5b859b15790859fd1acf966547991073dcc8a" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "lookup_acc_eval", + "fe_be_hex": "2745dca0940ea7c9f9ba6d334df08a4d7fc17d8f2e05eff69714a0a0e2e81138" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "lookup_acc_next_eval", + "fe_be_hex": "5a2ea9b7e636a5d882dcc25816b49ee55deb2a2fb8897ba937b3561c4b9974bc" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "trash_eval", + "fe_be_hex": "02ef94bc929648937325f43a95952c452fc26aedb755e540010f596921138ffa" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "217365ac9b9989629991c364f2b86cb3cc7e2dd253c7d8c337eafb54f76e6cfd" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "66f094b9a69a0cedf60eb8ddded60b38f2a8fefba6aaaa036d2f4b6aeb796ca0" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "55bf072edc9e8b29a7a17a8d603af9b3abcc82f7d82044c39d67176c55484c1f" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "02468eea3073e1b27af48d3f41f26c1507f1ca64b176ff5cab1277ec3826efb7" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "2cea1c7a2a07304780718cf0db14b48bcccb4faa81db1d238019aa7b0028dd91" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "734f5438da7262afdb9bab58ace48bdb59881211e83f08495ab5278c943761e9" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "43e6c7a470c1778905f81b8f1a9e30fbbd5ede196bb6f05cb3f2ca4f1569d2a5" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "0c329ee772aa7475d856dc6b36d22079898f5deb775ee7e0063c8586bf65f373" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "0ac393f94e8beb2695e0d96fac46c2b87df5fe8fc9281414481b2b425ada652e" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "545a2e0413f0b127fb7efb0edaea97e6c5fa1bfe5030edc8b90e81cc3d5d3d05" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "57f6e58eddfdf4b1246102abcc1383f62c22410ec317a437e3c7700c76ef1e66" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "2091646961d0d09fddc51c6beb15f5cc30ebea0a6d1a0cce6cfe566c7c3204af" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "6fe9c3598362fc66465fc26fd2d1302538e9ad9443f4381765fbb85a0c01ca00" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "23062471c3400021f4340394056f72cf07ff840cc8352c8264a25b476e4e3026" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "5b3a696e1a1aa7d80798e529ed5ca8c061f3f4441b6be7cf1044b3dd89d33b82" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "0fda02ae267bbc6089dc97650ce12345ef60da5ad037e125c7d018024ca4a427" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "0662a7d965658a0be8da606caa381247b9f24151a827ebf3a2b38807964115f6" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "7233bdbea510df4f392d617d65980ce045b4a69774f87dbde69b329eac3a1e25" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "395a81702231f8d3328870d8432625ef3d8affd5b42745d80bea35abfacafda1" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "59822a8396f70a891d061eebc5cd3fbdf83f23f0635b0fba71f57fe4ee38e3eb" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "15c6f19a446a5c45f27f1e2201dd55a06a0b1d9b1766d8b6af1ef040dc20f4e4" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "668c262534b2ce222ac07ccbe555d460a4caa6a71bd635c8f4bade9320f2e67c" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "5e1f88a4ca8d17644338d151a4208dc6848f2b2f1cb422318b3661f2dc7285a2" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "464111e94e086dc09c37893f983acdbda22a36199ee0ac478d9d4ebd7f86a678" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "153e361eaf1ae6fdc2fc5b3c97c82dd54329a250bbb425df88b69b4d3bda65e2" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "508d4ac75a6b644d3c1481b34c5e5b50c2ae9e7985ef4942ce0d0da35d9df21d" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "4bdfb4958fa24de1a0ef2388cbb6759550529fa4ec7c6f7adfa48c319f43d8fb" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "1ee1ab0877f49e49ab6a276088c13b36ec20bc5865c5a06ec1bed18c1f8adeaa" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "08b922953149a29240265228f597982d1775c278cf946478f45ae5f1a8525d92" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "0cb56e7fef410b9b69e1aa8c952dfa600c572f3f2f6168455cc87efc2fed8230" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "0847db79bc989aaf43d3dd1b484a983eef3d2af9b7f3da74f350ebd5cab8c2a6" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "50fc44c1ba7702a6d747da5d0b0364ef9fc3f0aa58a2a3904256235a8995a3e6" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "165281de2de0391fa23b7e93269ef78693e33d22abfad65ff1a42ac0c9e70e9f" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "593c9d84a62d20676a7897b297ddd00ae978b27af252ceb1080070f0bccd58bf" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "34d6ee7449989ac4dd64721f34516bfa18f7d9ebec836e3481b923abad0db2d9" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "16eaedc732a941e352dfebcb2213a3e3fefeae52072ba5754a71f7e84996a713" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "6f6c3e8c39f278ae1bdf82e4661cbff0b286efb4719c0303e54b40ee185a4301" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "5169390a3942987b5a8534222cd36cb6b5686549f3c6e9aafee37989360bb2ea" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "114f02402bb4f7c9356e1d6dc3e0a9f734c3d00dc33ff3192ed5bc206cf9be4e" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "25e04962c869ef9c5a4961f389a2eb62d3396c0f6b003cbe57af7e678f791fb0" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "3fce51a692908414ece339e02af9a82233f847d5c95d7de4e88b6b973989d2ff" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "1832ce0456e1acb2bc1caf0a377ececf48e786098d5bf5b1a5a49b0c98d5444b" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "0f9f1b10b091155f93ad077fd099116caafb8290af68a25b7ecead2e232bc3e6" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "34d46301440323ea9e9d34b24854f0fe39a319dc56354c11d12e37191e755936" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "171596ed4c11f4fb1825c822e7e55cf9f524d6e06cc60bafcca84a02a13f2227" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "3214669c09598f4633047c9362eec1f56f8868f1e1f4a8b6bae489535283dc82" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "6d8a8cb25435b745f1493abd9e1eb08dd1375eaeaf2be9cbdaa1565456d085ca" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "557fe05684cedd5f2a647d919f507716990ef42dea34063ba8548d95e1615e29" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "1faf9ef85017b00d38afaef9c45fc01efb0396756a4c7d30c9010bc5b3eb85f6" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "51ce729c9afbc07bb172cb93f7c44cdf93ea8b516e6a0bf85ddc08a32b5a1344" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "503976821440e034de289cc3858057c0afa0405d676aeb0e6ed6bcc91c5ada5b" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "217137664a8952ebd316d1cf31fbc2082c9eb9fa3a917848d2602819fec8ccb8" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "4a499803a7a039636e9748cdb55580c11a62cfbe5f80633fd5ac02c9648f87a4" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "206d5996149fccef1ffbbbd6a180c1ef5550fb53adf6895883f7f004d8c182b0" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "634fb019d7ae3e3a70eea76e60155ebbc2a38be2789a7bb01821e4066560352e" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "6d73e07158fcdcf39012ee7a904662d439d09a763fe714563fe54df84e9618a9" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "50ecbfa690b837bb0a1c8b133761b4d1d8bcc86b92f20d6755e60f22a45798e7" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "2500ca4d0e075a88c7a96589f5aaee896be02719ea54af9905ef909a73d2c9b2" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "dummy_eval", + "fe_be_hex": "2618d7274a3f02bbe241561a203140010cdd8606a92137b5bfc369fb2836392f" + } + }, + { + "kind": "Challenge", + "data": { + "name": "x1", + "fe_be_hex": "19dc7c4cc8e3530fa52fa6284efd12cb2b6b9b6357ef20a90cbfb3a9bb332137" + } + }, + { + "kind": "Challenge", + "data": { + "name": "x2", + "fe_be_hex": "50ee25d4f5e87dde2ef8ac0fe9c72bb57f26a76a3be4a55dbae87442dc6ed1e9" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "f_com", + "eip2537_hex": "98f8b67c44afdb3c7583dfcba0db6ed9405991feb288fcb79f8cf181f0a32882be961ffda067513f57175b8e8e44ee87" + } + }, + { + "kind": "Challenge", + "data": { + "name": "x3", + "fe_be_hex": "1e356d041d4473b4c1a393782c787c1f0f4c18a172a649726d2e34c624626f1e" + } + }, + { + "kind": "ReadScalar", + "data": { + "tag": "q_eval", + "fe_be_hex": "683585f89b71c600e9014d5d5cbe763ed87bd7ec34ee161493a68acddac0c89b" + } + }, + { + "kind": "Challenge", + "data": { + "name": "x4", + "fe_be_hex": "381c9934815b628c9ad9d9f9cf4882700050d2f894850bf1d925497db53160a1" + } + }, + { + "kind": "ReadPoint", + "data": { + "tag": "pi", + "eip2537_hex": "830590fd9d9004dadf297def15aba63c6eb7f6733972da094c6ed28678c341019af2aa4d1b6994a4a17b782605926375" + } + } + ] +} \ No newline at end of file diff --git a/proofs/solidity-verifier/fixtures/poseidon/solidity_trace.json b/proofs/solidity-verifier/fixtures/poseidon/solidity_trace.json new file mode 100644 index 000000000..28f958f6d --- /dev/null +++ b/proofs/solidity-verifier/fixtures/poseidon/solidity_trace.json @@ -0,0 +1 @@ +[{"kind":"ReadPoint","tag":"advice","eip2537_hex":"81e60cdbf67ce5879c7c24b3d3413304dad21febd91bee73191acebb2ed5597de2ea65647177e202c983122854298ed4"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"8e8bf0427660ee129b3ac742b997adf709272d75363146d0880dce510e186fdef0e49e65d30f5304a203642b965b6a31"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"b30df15f939f9f1e19ac2bf52b805ac80e1379e8fd0a9c588404b95cec2b014f444e396014ce28ab497b5cc2aa941736"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"b4908b22c8819acb07f0d59150a7118f3c0a2d3b97bb0b16175fa2ac34020077553e5a94a5a99d5809c03435e9aa5a76"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"83fd77a149152709931d80ffc6e618a834fe0110d53926df32c8728290432217817fd8dca035511a09d4a52844f9c492"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"a23f3a922265548de6e1e3607311b7a3867156aed5b30b51bcacb21e9a2f06a01820189deaa9fdd1f9cf60dcde5445f5"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"a61599aeab3384b4d6c638d561deb71a84e59cd05365cd5bc2b008b1bf797e4d6862ac0052b0d64f20753dbcd29e8b4e"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"85737395a12ef01dd8fbd54a995c888b32cc74d9a4cd8093ca75e38300f8122a5c814dd10afefa450b03de1f1263df2e"},{"kind":"Challenge","tag":"theta","fe_be_hex":"5da79f088ec824cf983df50d2c7024ee2a6fc2d14e245e3f4de569f1c34325a2"},{"kind":"ReadPoint","tag":"lookup_mult","eip2537_hex":"81cdda948e0df2d8dc2bcbcb2a216a44b661b35bd58219ce50d17e8b9c266d1829adf00fe5c840f920779226f0a1392c"},{"kind":"Challenge","tag":"beta","fe_be_hex":"0477a5891ccb8eb3a708632c708a2b7bb5f059dc95e1bc25f44fb417ffa65c4c"},{"kind":"Challenge","tag":"gamma","fe_be_hex":"6bd3cf7318992ee1dcb6e5e5f7bd78e122a1453318c7087d3ac00045ebd1aab8"},{"kind":"ReadPoint","tag":"perm_product","eip2537_hex":"b9411cd094ccd2d23defbe63850a39a796ddcdcef44f7fe1241bb6505d56fc4178f4ecbb5ec6ba5014dc4ef11bc9517a"},{"kind":"ReadPoint","tag":"perm_product","eip2537_hex":"8da59ef4d22efa935ea36a228d6f71008adee8ff0b230d2538320b4d467b35a6f6e909b6c781bedee2dc9b7e744855cf"},{"kind":"ReadPoint","tag":"perm_product","eip2537_hex":"a45f4dd167ca3437171ad6d4b982403b44f2866132141b865fe9abd0b055547d3ddc3919db874e5be9c3413219d1493f"},{"kind":"ReadPoint","tag":"lookup","eip2537_hex":"8812729949bd06ce74d7ca0e1adc1025ecb636db79cf8e3711361b87357a748f531fe71995befa9a2b033904e4f3d697"},{"kind":"ReadPoint","tag":"lookup","eip2537_hex":"87f8ff32948633aac0ff020e896770d4774cf90fa519fba2e8b50c0342e0effefd9c4c37523589cf84f9d8e399afe3db"},{"kind":"Challenge","tag":"trash_challenge","fe_be_hex":"3a4f48dfa840a95f19d2812de2e376cb6d2721edf593f257457f133d5a0dd195"},{"kind":"ReadPoint","tag":"trashcan","eip2537_hex":"a3b09d6ea5473d836e08e82f77870a56cea906cd667632771fb78b207a074a0098782a87ef02b15cfa7266f159e6135f"},{"kind":"Challenge","tag":"y","fe_be_hex":"61e192f45558aa4d0fc17f09a9f2b2ba0b9fe983c07f0eabf714aa039c4e4784"},{"kind":"ReadPoint","tag":"quotient_limb","eip2537_hex":"aa755f4c64bde20de81c6e98257562e8aaf8bc4bfb60d51fad0f418eecce3ca00d5d95d77abe72a8727f38be78584ee4"},{"kind":"ReadPoint","tag":"quotient_limb","eip2537_hex":"b0d93216a83f47e689512965c5848f8c39b67eb858c414fe6427a491ef7f7cfef561d566caeaa86bc45cbdb95747dcf8"},{"kind":"ReadPoint","tag":"quotient_limb","eip2537_hex":"aaabe8dd6047da8923e93cb0f643f933a6acc0b2bde278eca4ee4ed8063431a13d9dd1b93d3be13822d7866ff3464609"},{"kind":"ReadPoint","tag":"quotient_limb","eip2537_hex":"a3fe945b20303eeb1918026034ade07ac206008d6bbde640b0479e4b5a2ce78f82d342286f0035d5674c65098c4cc742"},{"kind":"Challenge","tag":"x","fe_be_hex":"516a5fbb000a58f7f6b39658291a583eeee4deb9452ccfa76e0e6bc648517789"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"517f20dcf80347a4af3efb14feb1920c29e8f35b7c8d037763e519a5bef9caba"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"3b6703300fd2e08ed62718bbcd69dd0e7f3314109b53432612f99ad4355435d4"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"6b88dacf5c9ade93ba2cc6dcb43b48daa909dabc5650fe3aed07e7c6031aa241"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"13fa71a2451f245f8c353491c945b5d43308373549dcd752f144b8c70fc47487"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"51ac7df4c88715664d1770483fb75d61e5551f9e58635d87f8f73ab1f0dc3b09"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"41f5b65bca090e64641e472af997515ce1f9c363478cef7abb8153167f354284"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"331897d366b979cae0be18065a4ae3fde5353503b884ad534c67cae68e8ff885"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"1a3388bec892451c1df4728a9443dd0f7e72cc5bf61979d1714c113f7c46b979"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"526f8b9718f6240618d06483776b09f7a746a2435f5a2ddee65abefe4046dc78"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"06a3c35c61e9d8ee13cc5b41cf1ba0a246eb65e9c5a9667a30c6be5a2572d4c6"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"0563630d40928737c052e61519b691f8426b82661400178b46aea061e4ba71b6"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"268a9fbd4efbd107a8e7010dd2eed90b5a3190acdb4ce1754970ccaa1ca5e367"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"700063fb2571a376cdc61ff188811118508dfb4302372e35ab85b993cf510b0f"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"406b9bb2bd3c47a83f4edd3b877a4adf682366966d11a734a03b2972c7adcecc"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"31e6dd0a5392def7d00614b7b4557e76152058a7c3dd9c015c41b3d601ec4028"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"2dad2ba5bb3ca8efe21aa1c9c14314baaa3002818a668ff19d0ffcae5165f9a8"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"0000000000000000000000000000000000000000000000000000000000000000"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"169ff8aa47c0e009f9a68d10a64b8c967f2042d65c00b23bd04d7ea946118e4c"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"019ec56c15efd12597c4bd0dd22c5f909cb2734f646bd1559f6b170a3bb4ec9e"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"21ab5bd7c14989f6f48347353bbe321d6e640f8465ef1ee1707e7a6772be711d"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"47cf90a730c293c4c10317f49675661f29ec21ef09e3f954f5acfd8e75c62449"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"0000000000000000000000000000000000000000000000000000000000000000"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"0000000000000000000000000000000000000000000000000000000000000000"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"0000000000000000000000000000000000000000000000000000000000000000"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"0000000000000000000000000000000000000000000000000000000000000000"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"58bdeb65c48b1e9e80eda0ec92c0d190103d6dc8cf8e1a45f7c00f050fb10481"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"46503458bef568da79c402fd3bf3688b961ef06b93f58a214d7610a51a6f837d"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"0a4ba78937bd1619696ced83668e61fb7faffbff60212b39b507e86b45a1ecbe"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"35e914b41778d1011fa5a36aaae152f5fe4e9894b83c6e284f2bf066bdf839a7"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"5de3c6628398eb56b4ee98ebb90fec16819bc7d5cf56d032ec2735904af6ef10"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"651a37fe7a65f0b59b56f279ca745b55c6c1d1e0d8e8b2ca0c0849697b5052e0"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"50ed18e2e66005d348f47905857ab6387f7b79ac12fca39feeb23bfc05e49631"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"515268a1896100bb6dc0dbf63f066eb0f6eb2cdf303aa569aa1ce6d1801f66b2"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"163ffebb77764b4c9f2adace16d342431184ae65339cc2e57f114fd53c9e26d2"},{"kind":"ReadScalar","tag":"perm_cur","fe_be_hex":"05ae92b954d278df54710b903fb36301eb584e2fc603619f8f1e9496799b2abc"},{"kind":"ReadScalar","tag":"perm_next","fe_be_hex":"1571fb2fdfe4e5f074599009a09570c96b6f50727b688ae1c69ab322334222f1"},{"kind":"ReadScalar","tag":"perm_last","fe_be_hex":"46c78a97147c819048dc1a0838c5858764e32a0340a4321e763b83feabea3c83"},{"kind":"ReadScalar","tag":"perm_cur","fe_be_hex":"6dc26d441cf7210a7bc96f667d0839614656d4b6e6f44625d8489388d1f5f8e0"},{"kind":"ReadScalar","tag":"perm_next","fe_be_hex":"530bf6c6fcbbb58031e6bf33733f1cbb923639f351684b1f10cf852b362fa2bf"},{"kind":"ReadScalar","tag":"perm_last","fe_be_hex":"4d0f0489bf7cda84d334f3d06d8007e67305d15ab145dff6d4a148879b2e5803"},{"kind":"ReadScalar","tag":"perm_cur","fe_be_hex":"57c6f9f7d2f53cfed65ddcedc777c41aea0836882214ce362dec8b98f874594d"},{"kind":"ReadScalar","tag":"perm_next","fe_be_hex":"230340fc9bff6f0403cb5d9623a91bf19422ad38b271bc9aa76aa54c7e48c52a"},{"kind":"ReadScalar","tag":"lookup_eval","fe_be_hex":"3859841e32e17933d3d6376476cbc9fec6fdac6dd1770c5c56682dd38b1bcb81"},{"kind":"ReadScalar","tag":"lookup_eval","fe_be_hex":"51cf8b7af274258426f9baf4c7b5b859b15790859fd1acf966547991073dcc8a"},{"kind":"ReadScalar","tag":"lookup_eval","fe_be_hex":"2745dca0940ea7c9f9ba6d334df08a4d7fc17d8f2e05eff69714a0a0e2e81138"},{"kind":"ReadScalar","tag":"lookup_eval","fe_be_hex":"5a2ea9b7e636a5d882dcc25816b49ee55deb2a2fb8897ba937b3561c4b9974bc"},{"kind":"ReadScalar","tag":"trash_eval","fe_be_hex":"02ef94bc929648937325f43a95952c452fc26aedb755e540010f596921138ffa"},{"kind":"Intermediate","tag":"evals_signature","fe_be_hex":"3da1510066bbce93e7b134bcd6e80da9df0e407eb4ebf974cb6f726fb3f405a4"},{"kind":"Intermediate","tag":"partial_eval_signature","fe_be_hex":"50dcb6acc73e04145333674f1212465503aba992e32a0d8d464b62f8a5d98ab9"},{"kind":"Intermediate","tag":"linearization_signature","fe_be_hex":"04487361fe26160444b57f4e5187f442a19065f71bdb0256bd13afa610eacf6d"},{"kind":"Intermediate","tag":"query_list_signature","fe_be_hex":"17f3157c7740a878f5bb4b2cfb32be50a68b738f672a51ac5ca623ddfec7397e"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"217365ac9b9989629991c364f2b86cb3cc7e2dd253c7d8c337eafb54f76e6cfd"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"66f094b9a69a0cedf60eb8ddded60b38f2a8fefba6aaaa036d2f4b6aeb796ca0"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"55bf072edc9e8b29a7a17a8d603af9b3abcc82f7d82044c39d67176c55484c1f"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"02468eea3073e1b27af48d3f41f26c1507f1ca64b176ff5cab1277ec3826efb7"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2cea1c7a2a07304780718cf0db14b48bcccb4faa81db1d238019aa7b0028dd91"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"734f5438da7262afdb9bab58ace48bdb59881211e83f08495ab5278c943761e9"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"43e6c7a470c1778905f81b8f1a9e30fbbd5ede196bb6f05cb3f2ca4f1569d2a5"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0c329ee772aa7475d856dc6b36d22079898f5deb775ee7e0063c8586bf65f373"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0ac393f94e8beb2695e0d96fac46c2b87df5fe8fc9281414481b2b425ada652e"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"545a2e0413f0b127fb7efb0edaea97e6c5fa1bfe5030edc8b90e81cc3d5d3d05"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"57f6e58eddfdf4b1246102abcc1383f62c22410ec317a437e3c7700c76ef1e66"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2091646961d0d09fddc51c6beb15f5cc30ebea0a6d1a0cce6cfe566c7c3204af"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"6fe9c3598362fc66465fc26fd2d1302538e9ad9443f4381765fbb85a0c01ca00"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"23062471c3400021f4340394056f72cf07ff840cc8352c8264a25b476e4e3026"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5b3a696e1a1aa7d80798e529ed5ca8c061f3f4441b6be7cf1044b3dd89d33b82"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0fda02ae267bbc6089dc97650ce12345ef60da5ad037e125c7d018024ca4a427"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0662a7d965658a0be8da606caa381247b9f24151a827ebf3a2b38807964115f6"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"7233bdbea510df4f392d617d65980ce045b4a69774f87dbde69b329eac3a1e25"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"395a81702231f8d3328870d8432625ef3d8affd5b42745d80bea35abfacafda1"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"59822a8396f70a891d061eebc5cd3fbdf83f23f0635b0fba71f57fe4ee38e3eb"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"15c6f19a446a5c45f27f1e2201dd55a06a0b1d9b1766d8b6af1ef040dc20f4e4"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"668c262534b2ce222ac07ccbe555d460a4caa6a71bd635c8f4bade9320f2e67c"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5e1f88a4ca8d17644338d151a4208dc6848f2b2f1cb422318b3661f2dc7285a2"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"464111e94e086dc09c37893f983acdbda22a36199ee0ac478d9d4ebd7f86a678"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"153e361eaf1ae6fdc2fc5b3c97c82dd54329a250bbb425df88b69b4d3bda65e2"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"508d4ac75a6b644d3c1481b34c5e5b50c2ae9e7985ef4942ce0d0da35d9df21d"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4bdfb4958fa24de1a0ef2388cbb6759550529fa4ec7c6f7adfa48c319f43d8fb"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1ee1ab0877f49e49ab6a276088c13b36ec20bc5865c5a06ec1bed18c1f8adeaa"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"08b922953149a29240265228f597982d1775c278cf946478f45ae5f1a8525d92"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0cb56e7fef410b9b69e1aa8c952dfa600c572f3f2f6168455cc87efc2fed8230"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0847db79bc989aaf43d3dd1b484a983eef3d2af9b7f3da74f350ebd5cab8c2a6"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0000000000000000000000000000000000000000000000000000000000000000"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0000000000000000000000000000000000000000000000000000000000000000"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"50fc44c1ba7702a6d747da5d0b0364ef9fc3f0aa58a2a3904256235a8995a3e6"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"165281de2de0391fa23b7e93269ef78693e33d22abfad65ff1a42ac0c9e70e9f"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"593c9d84a62d20676a7897b297ddd00ae978b27af252ceb1080070f0bccd58bf"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"34d6ee7449989ac4dd64721f34516bfa18f7d9ebec836e3481b923abad0db2d9"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"16eaedc732a941e352dfebcb2213a3e3fefeae52072ba5754a71f7e84996a713"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"6f6c3e8c39f278ae1bdf82e4661cbff0b286efb4719c0303e54b40ee185a4301"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5169390a3942987b5a8534222cd36cb6b5686549f3c6e9aafee37989360bb2ea"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"114f02402bb4f7c9356e1d6dc3e0a9f734c3d00dc33ff3192ed5bc206cf9be4e"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0000000000000000000000000000000000000000000000000000000000000000"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0000000000000000000000000000000000000000000000000000000000000000"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0000000000000000000000000000000000000000000000000000000000000000"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0000000000000000000000000000000000000000000000000000000000000000"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0000000000000000000000000000000000000000000000000000000000000000"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0000000000000000000000000000000000000000000000000000000000000000"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0000000000000000000000000000000000000000000000000000000000000000"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0000000000000000000000000000000000000000000000000000000000000000"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"25e04962c869ef9c5a4961f389a2eb62d3396c0f6b003cbe57af7e678f791fb0"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"3fce51a692908414ece339e02af9a82233f847d5c95d7de4e88b6b973989d2ff"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1832ce0456e1acb2bc1caf0a377ececf48e786098d5bf5b1a5a49b0c98d5444b"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0f9f1b10b091155f93ad077fd099116caafb8290af68a25b7ecead2e232bc3e6"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"34d46301440323ea9e9d34b24854f0fe39a319dc56354c11d12e37191e755936"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"171596ed4c11f4fb1825c822e7e55cf9f524d6e06cc60bafcca84a02a13f2227"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"3214669c09598f4633047c9362eec1f56f8868f1e1f4a8b6bae489535283dc82"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"6d8a8cb25435b745f1493abd9e1eb08dd1375eaeaf2be9cbdaa1565456d085ca"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"557fe05684cedd5f2a647d919f507716990ef42dea34063ba8548d95e1615e29"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1faf9ef85017b00d38afaef9c45fc01efb0396756a4c7d30c9010bc5b3eb85f6"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"51ce729c9afbc07bb172cb93f7c44cdf93ea8b516e6a0bf85ddc08a32b5a1344"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"503976821440e034de289cc3858057c0afa0405d676aeb0e6ed6bcc91c5ada5b"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"217137664a8952ebd316d1cf31fbc2082c9eb9fa3a917848d2602819fec8ccb8"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4a499803a7a039636e9748cdb55580c11a62cfbe5f80633fd5ac02c9648f87a4"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"206d5996149fccef1ffbbbd6a180c1ef5550fb53adf6895883f7f004d8c182b0"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"634fb019d7ae3e3a70eea76e60155ebbc2a38be2789a7bb01821e4066560352e"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"6d73e07158fcdcf39012ee7a904662d439d09a763fe714563fe54df84e9618a9"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"50ecbfa690b837bb0a1c8b133761b4d1d8bcc86b92f20d6755e60f22a45798e7"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2500ca4d0e075a88c7a96589f5aaee896be02719ea54af9905ef909a73d2c9b2"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2618d7274a3f02bbe241561a203140010cdd8606a92137b5bfc369fb2836392f"},{"kind":"Challenge","tag":"x1","fe_be_hex":"19dc7c4cc8e3530fa52fa6284efd12cb2b6b9b6357ef20a90cbfb3a9bb332137"},{"kind":"Challenge","tag":"x2","fe_be_hex":"50ee25d4f5e87dde2ef8ac0fe9c72bb57f26a76a3be4a55dbae87442dc6ed1e9"},{"kind":"ReadPoint","tag":"f_com","eip2537_hex":"98f8b67c44afdb3c7583dfcba0db6ed9405991feb288fcb79f8cf181f0a32882be961ffda067513f57175b8e8e44ee87"},{"kind":"Challenge","tag":"x3","fe_be_hex":"1e356d041d4473b4c1a393782c787c1f0f4c18a172a649726d2e34c624626f1e"},{"kind":"ReadScalar","tag":"q_eval","fe_be_hex":"683585f89b71c600e9014d5d5cbe763ed87bd7ec34ee161493a68acddac0c89b"},{"kind":"Challenge","tag":"x4","fe_be_hex":"381c9934815b628c9ad9d9f9cf4882700050d2f894850bf1d925497db53160a1"},{"kind":"ReadPoint","tag":"pi","eip2537_hex":"830590fd9d9004dadf297def15aba63c6eb7f6733972da094c6ed28678c341019af2aa4d1b6994a4a17b782605926375"},{"kind":"Intermediate","tag":"multi_prepare_signature","fe_be_hex":"16f4cd32fa883443f0ba14089c7c06d2f747750e7eca8474b2534ffa477b1a5f"},{"kind":"Intermediate","tag":"final_msm_scalar_signature","fe_be_hex":"5e5558c7e7d739a585fa10215a9d6a437cc9fc65c35dd62adc9a0d3955f5ea47"},{"kind":"Intermediate","tag":"v_scalar","fe_be_hex":"1bb71d8feb105bb889bea4979aa153c6c32d488234785979441815b5921a3b78"},{"kind":"Intermediate","tag":"fComScalar","fe_be_hex":"000000000000000000000000000000000050d2f894850bf1d925497db53160a1"},{"kind":"Intermediate","tag":"gScalar","fe_be_hex":"583689c33e8d218fa97b33706f00843e90905b80cb860285bbe7ea496de5c489"},{"kind":"Intermediate","tag":"fEval_sol","fe_be_hex":"5868be76f4ee7c024c29170b09c904c19cfe50670be4334fd03eef23dca10266"},{"kind":"Intermediate","tag":"right_g1_digest","fe_be_hex":"1e22ff6d423eac68310c6728de59d2cb7a1021cdef4800ea805b101fe162572d"},{"kind":"Intermediate","tag":"right_g1_len","fe_be_hex":"0000000000000000000000000000000000000000000000000000000000000080"},{"kind":"Intermediate","tag":"num_sets","fe_be_hex":"0000000000000000000000000000000000000000000000000000000000000001"},{"kind":"Intermediate","tag":"num_commitments","fe_be_hex":"0000000000000000000000000000000000000000000000000000000000000027"},{"kind":"Intermediate","tag":"final_pairing_result","fe_be_hex":"0000000000000000000000000000000000000000000000000000000000000001"}] \ No newline at end of file diff --git a/proofs/solidity-verifier/fixtures/poseidon/verifier_trace.bin b/proofs/solidity-verifier/fixtures/poseidon/verifier_trace.bin new file mode 100644 index 0000000000000000000000000000000000000000..ce34468706532112e264104bdb3bde624224e86d GIT binary patch literal 22515 zcmbVU2|QF^8zzYs`%+o5ltd*AAwpy;R4U2x*I?|7Wr!3N5-Or36v`6G7b+q`wxlfW zB}+w?B$chC@7}q{m@_lCxxZhV`#$e;?m5qW&wI{&Peyx8Nw%M`nmV$ z;v`kB8pwu=1ZIOHU}50=gX!TykIocivf8fmh*%qN`|JMJ^PTr3e(bol^iPJRA!Nf% z0(8K-xe<{rPaD0LU6ko;z0=h-TI$y+o~nacR?B*1Q#pf9e{*&b8Is^Q#c7q9%zCD7 zRb%|-VYYQ1tG-C%E1Qj1{ik8nARC@JI9IGU0fvgTl!y`Xax#}lfALD#@VKL|2>1Sp zi&Cb3;VX{b=Vs%O4euN%ar|MdT-lOeo?DOoW(-R>AM>tXN5T1%w($4Mr6XKnZB8i} z@EDRlvb&F)wY`%c&WD2>Ym^ zOB@rqyKv*zD1a4-VCW{T3)I^iiD3TZv24Baj)mhHr#UmWpNb0-)p}vckgwWg6qu#U zbq0QWKwu>znDKJ8E8k$V#D%I-ww86)(MkEw)uf^C4gfPIz}2*wPzEm~F!Z+3?M_v32xucd>>7KXhYy z@`R0+&Nd=a;$3~*+-LiO%{gbA{ll!sH5>OYlOEe_Ao@9dRx!xmy3jOvOWr z{FjM}q~oE{gJvEX4==nO)Y+oUxiNS5wD`5}@mw6OQDDp1Z)Uuqh6$S-wc=cyf1>og zx#KnkcgOh=r&vDEUmMy#c6}x6k6xH^-s7PQey3M}V?<|7LV-9&OMHj|eEPX{Ya=f3 z9zGVW-zvbWTpb!__d%LTGfjSluWIn*^**&=5M1tWYz$qyal@7S z6yVd1H{zHde*f^Bkf#dYo7|4FbbB*5o9F4Yk337&4@;CWBa=Mbb@G23zprL|v}IUr z@`Y9FK<&XQfj-uaC&hW6Y+V02GJcG(-w4DG!KWL)l1XzS@eh7J(<7_6m05yA=frO) z=X>5u$aGFO4AifdhTsCTC1Z_#zg53GM#io9>JmE~ zA!g*unJ$12K0?g{GBqsPo*AxG%D>#nbs*UO4E9&h|9<^BAhuTJHcO9-PU`0}Y95g3 zL9v(j)>6M_Mv?TZ&e@$N!;{AG=Ze(a*X&sP)xLC0C6k&5WQwr*P-7Du-*0;BOML%b zj?etZ5^J9~nl{OQmQ;-1HZsUb%>yz`-~^N=58=MvPBi`IUr(^uTT{{8@v0}kehI6A zQ@~D>4r(5dsbc5AviIEwn4jwZ@MjJ^bVoTTz(0q3;%f4jeANl&k8#jagYsA))5R_m z$JB5vLy24Y+YyZg?l(vJ*R{)TN_WZ@>TW(NVxLdV12SdsYfP3L^xbZlEdAyQ|5Gh( zu8>?DCaJP3h-=4lwacqpAP<7G^#BK8oS%mm&f6Ob?6ZR~3uH+N zM)h2p&+|RSFY(Gi=#)x$BX2wt+P9`AphCXps?7;SmBEdFUbPms@43$A7Vh#82u0+< z*8EfO>`HSFZBgyL0-ZZ!&QA92Ia8=t{aR+VP(gKjDG-Y2LM}<;#%S}Y>^(tUx^3V4 z&&D4&cFS3>_~Kh#=cn>?l@mbdRPGS$c!+nPaoO&kgT;(@kSL93HQ!Y7sLl}M|7Q5mdd^}|SWqGy; zf&9aCx)4F2pW2p81E%96iscvEH0EEXGkyewwYBwe^Kr#O%XDH?gE~f3Ql~ZL9DC-& z@nScO(bov0po$|=GR%V`7kE=r-uH098WSW(!y$FXg}F>tVfV)LSI1i`&G>I!?y~Ba z-cS@Ckfl^$DzyC~0iF<-BQ79vf|G}UBJP1Y zdvp~CU+h-OyqJIGg#k0X`o%09_Iq<(6$;;d{drOQ>pU@?t2;V2v)DI`A8_|tB1oB6 z;a_=Szzi>kaae8X?_ZKC^UpHYU*}fsAAi8SioeR`8D8}T+Ue(BxvaKQ#Zv9_?$0Nf@Y_Z|YV`K7pA ztGDkHWnPtk<%Iz=yyn$MHF>|XRcz8)TeYt!x`07hwyyKf4d>BPmfQ*VXYgLMI5_-3 zJ8Ga87#5Z&6~qklG8yJAlkB&8!Z=i|ow>AH#8c(-4%S%j{(ei~$m^nsS5%k*A?pux zQDG*DStv4hO3w@kS~QqRq85t8pAs`5Y|&sQiCc=a*A0}I0fCDKGfCt^vFxD43{aD_$X`zNn#eHns6Hk-pkIRLF$6$^6gLl*UTD#-X1r4+Ruj_SucTjb??nS<$p?YAfi; zZXV{*rjjHPlMi2Y2isIGTB|OYP-%;Q;T-+sSIH%m&XCO^Ya`v*8zPGyCcF$Pdz8Ma zz+vD{w;=v-gG#6HqCLOQ!png*qD0Ty1pn`w_1vNtzB z^1bu>|MAFM?@j0y6=YSeKQ8AJc=n^7@459Z(G3oHwEZA^wbBgFMY#lj=OkP`#`?8! zzhSGY(MSCwUtfr4tBILDzjGGyBSPW1T(LGdSL7|7mK$VE!X={LG9$qHs(c zX4#vs<@v8^=yiwhlEPJoM4eK(6If(c#XD&ynRa$C=@1k zR~JoJ5Tzq~b@H5rsH}Sb@@4Z5DBS2cRJnlRv`cBX=1slJ+s)T~iaP#OYYm=3@wtN` zZs94zS}hbUCW=h<&d~kk1tx*#I+EnIj9zXC++2(w^R>U(FuynStEpQ{O88HW(U=D- z3%_mWUGg?xb14ePL^;ZqLze$|(NYCbxUx61M+|gg4C<~=Sf>hJ`cLYLOl-&VSd|;4 z4;!w>_hY4hP$OtLCQ4j3mc9KdccN*2UuG{;xR;vol1{dW zw3gn$g%wv;Ty@y%KIRtv~`S0FYSNozj!C}VwOnte787!n(3fz*YfnT z@%myQDKZDrLy90(9hhV}S~||WkX%;s*y$a65QdjCakutr?(u|YihC4^6=t)Vi##bZ zVbVj2AYD$rX|zmei!EMP(|Su)|4r7yH}_0;PHq}t`BcLsaAgbdNs-x=9#RC!^V9Q+ zj>szOQ;G^_AL`tGhGA7wG-!QkGPJd*mZORg1|&tMVtPoYkU2+w)P?cc8!h$F*C@2Y zcI_3pY-Vc}Ha5B`?`x|n945$xgv{HXMLV8kIkFe$z82K0ZrUg*yT3!{TlOH=HCFCG z-+Jpf2uda-1i5ASjz#@Kc00QjdeY+;c3F0gKd{w1HF&7x*lzwHMJ+hxCnrTFd3s0@ zBxc!#r}lUK>pRMpEim8q;dhs!B2!VwjopS0_1$Np#^7yXa#AECpobJe3Sdr_)y3!t z3IFhlIH&!NBhahCVr030v!@}K#$nyNaE?Syiqr|wGo}dA1Rr@}*~qE|%bM(h54tQI z&D6c3ZEe23t=qh8Qd74TUOthNBBgBfkRr$hS07kndtZIwX|UMRD423Zw;$JAon?PW zwx-Nn!s{7aGRsT$o=%m(p9MvT4SIDV3Xa^DVz~c1+9VxIHh!(n#Ih z6zs=h&pTjR)Io`y7J;HD%mF+t5?oNIiJTUJqA2JA(4r1ZF8iv_>yU#>z{zP5D2h5M04)+|=%PiSC~C_9w5Wp=`O_j$6rcaeX`u}^r~}cQ zB_N}nhSDw!0MODOrJ#*DxWy<6E&#OjM=5y(ilRsZKudp=l1HE@KE4BJQ72I3V~aph z91#G}(jTSd5h#ko1przk)X)AEgu!D2l3k04@Dd zN&$hQDAfnh(jTQ1rl7Rz`vJ7{M=1paisDcJfR_F!rGP+Dd?Z;;te&=pijASNDID=! z8N7D`6;eYXZhDj12l_iFsiB`7^fS@rt%nWkKJ4a5-;lA!_&4{XwJwRa`vyg`mfSDM zD#3^;0dVp#Zs!+?XTv>5+GNIZIC^W>-VR!w>8$!O-Kw~Upw;T$+zZ(Vk@CSGe336W zbB?{iU#~<+ZD;JD-E0|er$cb{^z@^^r-6hin@{h5(>_d2%?T1mQ0#|LbAm(#6btXU zBZGk+Q55<`a&Z_gOZCby@N$z;s99x$Ujf~m(`&@C1!8@(w>5M?$q#lj-D_|s&uObC z(G;NQ8j(6L0%8irm*BP+`?U-ef3FFz}M;pdIbhdT2VIEFXH3Kjl`bBa9dY7LR?l22&wlVyH`f0JKtN@G$o*_g%I@;ps zqZ9K&OFs1(xOG*9L$2p^w!M=(^gknbIXO5&6(uAGI^oxF&Z~kLyW;HU+QOqKN!l!U z&Q-jI{gUJqupwy)1!P0*f~n^;bG8rUg_-LB5KXA~K;D7mvBOLjs>1pnR z?eNP$rBA`hE}O9A%_72Od5On)A16GYW>NQnWUi16$;IT}944~=eD2jd-)x-tlMA7n zRN4VvEEmrU^sunfW-oYrU&A^18z1Cj&U-_mG4LULDRyhgQ`kvqsKl#NEdDn9X_J~0 zuu|_Pt4hAyR4xDdwT5$Wpj`_MCt#_L27V209J>1A>4C~c%UAbu(r^OSsyu^y{;$}R zHuqFUV=-L1+%%kk#agvrO}O*qnoV7yCI4Nzd<*{aO07@8YL(#P)APuy63=TtZ5QZO z$VxK-bmSZvW_$T*D3=Jn>#fCO3OAbYaf0+L}QH-tczcd<7z>4KdkK(gh^4N)82!4v>kGL&3ojkbv@%sfogW z{shQ70a&n}0O=+G3lK8Q2{bY02Zt&K;j6% zf@KBB7XetXt^jEwITn3(2*84M1xN}3Sg@`DxgY=w))gQH1Yp6s0%U#wELc~7gb#oP z>k5$P0kB|Q0n$4F7OX2kRtLa>bp=S~$g${iH~X%GBV^MW;w<=Th9zE=G$Yfoy5 zOZiPc7URC22c=-ofzes?UuJm{mE`uwGMp0WR30Ai;; z>gff=GIA#$Rgo=?NcV|pd*0x4xtY!LzC6EuweJhfJ&|B6qAQbt%s-HnOIVn@!@ zHchDC>e|rX)0Xr-$F?U{tQlS=k+*s3^BP0>A67#A2Ydf3)@pppDc$B#Y+YQUHb>VA z73k&moq;#x09Zs<-mOCT6z2}c+GLIRNZ}p2p?TYa|7(li5k>iF}FMu|Kbp_ZM2VlXv0&H&su!yc`ZEpV`Gd^*l literal 0 HcmV?d00001 diff --git a/proofs/solidity-verifier/fixtures/poseidon/vk.bin b/proofs/solidity-verifier/fixtures/poseidon/vk.bin new file mode 100644 index 0000000000000000000000000000000000000000..faae9a47c8127e44d78aa41721ea5990966d1174 GIT binary patch literal 4128 zcmeIwc{Egi9|v&Ej5S7<22*~PLMXzJefe23vSyH7iR?@EA{ruVp-hrpwrqpRzBe^l zvPKdyJjFC)D>L?o^ZfO@&+~u(JfCyk=brcd-q*RGbI-lnv-Fx>#X|o{Z=ed_53x^V z`hKeW{!jGYX4&?4S8%3Y%}u?Z5JzL4f`tIrsr~i~&+Z#7Y@4HEw1(x@$|=|A{xe!f z7sjJ7ABFoU3`aR0`lCRO0zS&|8QW2yM>#$}K0D6web7;kpE>TwbKL*uoC=amHkCgE zUut_O(vpr1c!!}fQpt$yFHVwpvupQaZ>YRM8(Y!l$Gl^8sta?DclpO~e~^ij#dvjd zqNkrNg~*994SIcfu*ux76Q8tF_&a}d0x#_yNgwM;2<8+GPz(`Jw;&Bv`qN4VlA&RikFK=u6msInpNMI4W64;UJZa zfA8V6i;TYzJ=xaI+Qqj~&01&{J0RrPuQ(CA1@!#sLT{JXsnpYM_)5|ij#Nx5^?d8l zCjH{3a2eV~l0wOGq07$m3F2#UCi{zMNplZLv#6)MmR&PgeV|dgxGlg(KJ$RbpVu^+;{c&; zv=He8NsK!I6+M+%`R59Z#)QLYkI|?IZ@EuVo>&IE$eBU6r24SO1zp&# zaxe9d2UjDmBZN*00zKQtIQsGGD_HGyQz_x{ca_dIHwicxGk&SPlc1z~MtaO3Z-Q6W zX2A)1Tkx)g#(({PT+CRF>Rumvh*UKfGAE*&w~1Yb+J2c_p*F``wplhk*N{MwsGNeI zr#4~Bz3DxH@Bh=CQMDlp8r!VF6de`<-kN=TYofyX{Vg@Sl_E{$*(&>n83XwG?`(Mi ztCjK6zofG|f%~%O^nhFiQ=Yh7$RTXMIdIx)46XfSIcfVjRQpG# zNG*Jsx92L*|G)p)FK}wTlfT{~D)U>8oR0CNOq6LaC5Gf8`KxZQj~g-j zC3N(T1jcdXE^uF{xAwrT>d>0=-}x>^(`A^oW}=%%1g1_`OlU?~cxy^Y^o-R~t-q3=w2L{*47yWoRT zi@dRTyerVN#n>fITYU17{%E8ljM*N}*Y8CIVa(b-;Q70Um?^>?S)#5Fxo@XzVcNgA2>)$cSS4JSoujqpd zfu4!%IBnl$i!>wpXZ1znO59Wq8=YX22Yt_kukadL;Px77>fCHQ%HnL^MD{HD&jLL+ z)u89${b4=HyD9r)%CAMEwH)xf$&(xLH%_ucW|;cJ1%;ywb?u%X5OouFj7KOy&zeFM zsk^O5fE((mD&(htZ-IEW5G(}ho#o}v zMhVrd`=W|cr5e0g;!$=A1@BG#_L@K1nSLno*w27L_=SZwUYbM#J&b14i*gS1D5C7F zfL%2+V>sSt=4_}Vts`-3$T7PaWod%8_j@EI0P#}>n}&P~^eo@El|;SS4jpQS!7&f4 z9ZJ&9JMTdkSL#n2WZO8sx+b|+;>c(EnawXNySDIpGY`-+vur*-gy?rS|NUTmQ`z`N zYZ$$b%bCrDw#d`*JH*+!NR!@X&z6SdgMMrI5@PWL&@(z-7i|&T&=K{WT#~D|98c;r zO;y$T`870F4?7Sn-TM8?>S6eRJsQWR0TNKNj{tg@_z2y_NA^UA3JD?}qMWv)5RlCV zaw0|z5ykMA*q=Q8(nPlWOTWu4tix-ZQuzbu+1wGCdjyz6`rVcc6{F=S>nn5*hplAq zRg3KTVj6!h%_YA2KrK%?G~-9TYl=Y6?2V!57c1CC1xcOA_X*Kv7&p0rgVV~4pLT(Be;FFeK!^1a z=ufGQqRt*94b4fQhp_hCLx?j;#neE{U!fPoro+XZ&c_jcWV1JWyzyo8S~jJ`U%g`E zu2h9e_$bJ!4fH3SIoIpQR7y6tn0w>2Si;GQGn$px#%}nT=LAj8lU8pKsu1C>;31h_ zQiHI2V*${EO{VDsOcD2*`r!1m7a$Bb-Lu_W$d{tE2#J;p;f2HyP+}>2Cn3Bx#xT&g zF7^!2^Jg?~k~0%i1>xKoaXq&i18gQRfYz0HrEK^NGRKDkXRX{b#*-2>V3CxdlS%e zf0J^y2*`7ed_9(*AZ~i=y4UdVb0M5SLDn5Ip5o8BR5LPyzJZJH|-X wsk$LP-w8(8BVqOD%A$-uUu!tE+BARXJ+Kd(3)+~pbYF0UqGZoMG? z3~cfsvGDb0?#w&jdIX#NAHKfqbWWdFJxh2uVw0b-Lz~%*oh?;l3fm2A@>iu#KebR@ zekYlO*9n{a)>pk|FCQsHc+CHVHGH-n_2?_(y#Ju?f0sBm{r5jEm*(1-AgnH8kJWs3 z9_PX>PXjjSG-PsM(?9v}uegr_o{y`y?cIb;{^!ew3nj~RpI*J)r;JU$MD+dsqGi7y VIZu${#wHIEbKfg?t9W9%ECBg_d+q=L literal 0 HcmV?d00001 diff --git a/proofs/solidity-verifier/fixtures/rsa_signature/proof.bin b/proofs/solidity-verifier/fixtures/rsa_signature/proof.bin new file mode 100644 index 0000000000000000000000000000000000000000..1aad37ed5dd02a89caf25b01217381a5cd0cb5bd GIT binary patch literal 4080 zcmVQqZF!;!DMl$^vFvN&W=RN%s=p{qKRo)i4OON zlP|G-|5+ADT9e`D*iv~?6>9w^6%7%3Y= zpP@u%NN3YHDPrM`{Sn1d3)#!M8QTU8IbM{4;gT%ovJoz!xqI;G;0ltCzQwt zY@{fKr8+mMJ#jFtxO|XmFL=(C z1|mRM5fXGcDT8A7zw;*emxyhl6?N7Ml(35jTC6V|7Gmez<`i02qR`n0nV{wYLbkLw zPXvaX$UKOboP-Py`A;%!1VRHirl8v>#8KTFYXZr$n$wgiqxC%*9x6;1OjV`I9N6rj zyh-^Y^Td!J^1~G3kHpOfWWevk$TMK^_F%edqdGXn3MAuXjn9ZQCt~^I!L~#-5TTG!)JDG|oKMahAH7GPdKA)^N^GQ5A*00Z|h<*~un44EoW~GJ; zXId@9!09wE^2)tiKhisc-im^i{Z@e@Pe89I}8tE@$=WjG@(kKVA^2bx3VCiNK{(y01x@c(f z4}WJ{(FkG5d@(!QDyyyYBP28W9{7$Q;3*y1`vWc}33D6%K_hG`p}ZyFv+{14m%wu8 z$SZ!ZWMWdzZMQNeX(ixhU)-1Qd+R)(7j;y0pQUnmq~NLPG6DZ6cm?48urZ zNP;=URzT?0sPk?bC(c)ocZ zFVfY2C85p4aAz`d(cWbH0b7v2vmO8dyU)r6A-+XTFSjGkt#s9G-onR&=C6$C*L-8! zz9TbfhasKsVq-+yI%PuYxv@LIQjO+1n$C2d@OSS~ZL$-cff#&?9wLUyJPch%t!tu` z7-3*BBG2pUu0Kg6ENT!3KkNOOUN!9me1fIMCDB8|*(=+qs6`ekE3L${*E&Ph4W(+@ zD+)Vov<fMcz@sGS9~m$;a2#4xKmfJor&&%3|LRQ7v?e*5 z>4Uh^t4(5YyR=WzrJ&I|P881c-+bXXctrQNoSghuzKA6rlOPN_O>3d{q9gr<7r#|b z%CIXkFAR1F6o25yV?AJ5KgUxh4F|5G@@MyN6CFN6`$FIqx!1Q8w$8C?>Kl#CAI(8{ zA3Vj4984>G<`JVUCyQGXpfS!@n$PG+ltJ^%z>sf|P>G~8lkryzPn=TPEvN3cdHFG5 z1`eL2$6z$1oR?r^HnOD{Pu_4(p*8>Fv+p4h#N8Wh5m4KLPD5^O4SjFd&*h<{{Ubzh z89q+td~)2*SP!;b!JzXa8~=Tn-_>s7`nfW8p!0mT+>Y}VuJ^%t-ez_6>l|_VY1(dW9!y7IzjvpP z;<1wTbzXl}$X>DQmf8%~kZFTQsE`SKN>mL$4@_F9hVYFus|@?sl2m5)1ql=<1EA`- zVyDCY9LYD_nO(R!{0>zW!`09Mum!*+s|{i>*QyI!e=vHsut{&$zS(pKuOmTuKyHAS zevIK}s>$A&8)9UvyWU4|8%hCr=S85OT0wMOl)Sf#{u~SH%>RgKYuVmj7xp7NP3>zV zrfkJ{6rFbk-MoHsGSzAC31k$M82*_#>DBN9&Z;x?N&}^CXx|>A&iU5`HUZ5?ZXM}- z7^0%2OiOO1A6y0zIZd-L2%xm|9h*4XZJa3?6o#0>3hE`nCind z#LUlt_tSa`!vt^UzffYl2X&jg12oH{e*Ydl5xKQ&EEl&9Mqp%vnFe@%Ly9G&%d;M%_9-MxLd2sh{eL%;Bd%%8 zX>Fn8OP@d?FK#&%POSamr&1k)q8sjrs$IC8Fj1#D6ZsQ#dvdW%(oDdI+>4gKFkvnG zKr=&`L4Q#$9((3{f8H)wAdBVa{Ztsb`Bioz;KUJP*FOC-xG=nEK2u2iDcyzP2Rsp- zFmIf9#3i|-vZ5Uj->~`F_DF;h>=q1=$`&N~)r|1IJ;M)B+Q+e2an4^yJOd4Y&+v&g zJOtoTnZFM4prPG~WYkZ&%vNWd2Xb;?zR?JUOPKMIS0(hm4&G=2P2m1 zY$@#M2**mIt7Ys!lYbE4T69W-8;IZnbC6H&I}QtcNgAW@;H;)YO*>AlMJ~Hr=FKe{ zhM<5um0TQ-iru2WbiWe`I%26y3ogKdA+uoGPCb5RoznZCivtcxt8{TcWs(I3#cq@v zE2FNAg~jS3Px`ux+=E#K2OwX9VH}T#&!@7%+PhtI9@e&p>-*P~XGtp-FuX#oxM&2t zr(Vft_^pxfE33e*iahpzLoX*w}%oXDz!$(UYkrVFb;Fv6eF&qpnndpkZm` z6D&?t#nQ_eDB=GyxKMEV?D`htlDo&M+o~gagz6o@*c-xdjeiXPtsYN0V+=LS2?`Lr zKbAX#7(L+^Iupgkj3O_OZ?~;S!x{LBeDF|V3QUd|dG@CwGz%3M?uRL2s=spA(5kHp z5D>%XdYdCd$>7B?zvGWkN7z=8Z(GjNr=?O~D2i`IO8O((`1lFp6nE6%`RZVl(oBA5 z!)2%_a;t+=ePH|ulTEFKo7z149%0(ZHx-*}U1TsRsUPBL5Cp$wx4GZxIpTk}v0z6z zDAP_vJlQ>TJQIBSn8MAixR1S#yzq79Ar1lb72R0WuI3>7YPuzUs%3ihq-bnu&mOZ`9smcaK1QK4tjNw8MRO>M-;=WsN|P~ zM^_9a0uE^d$^1J5_oefq+W2e`W1+ELaQfs7to7>tmRxcW=Xu50Wz&L+X0|6Vd zV>!&uF@;6&-RgYrWM^l!>_F>kx{eD9T2F-J_^J>a?w@tIt9+ad#msoC3zlY28S&H{MmI?f=N`c) zxwy~*JWFybn{uC%Wwz!O?B#pO+gmK(5U6as+@|)Lz0mGJ92KNlQUc)xubXijZx!uU=aZyQY0m0}}uUM!$9R&_n;fNJA)0h0sTN zQ(bR&*LvqN@C8nftlHQP$@c7A6@Dl$2eRYXAUkH*dz8P(aW3V!3H+ zud)@8tQ5A~X^)oKa_w|qlb{um9qKS4KrJ<+%w$@WYF+S2ym8>)jLJjY~ETZ2N->}Msc!aHw5 z$LvTRS|k>Ri9@!0?>FX7&=hu};v?pOQ#3gZ@A(gY-`DGNzvq0;d7pdky|0;j&#~Fyz!wun<$ETC_dZLBI-sQ$ z&s*odTPjSoEih$>FF9dA%}}_7{P;~J@x|0<0SD@rGUw&5J<6CtlNd?|PoZJogC0E6 z073y`0E7Sp1{@IRQ2?R@L23Xfex0_@X=w>_En z+DkHZKPXoD>Ij-tdsAm&m36O}YHY#1OYRT$OLFjgK?gzXl=g^Sy@wKdPJFI{+pn`* zs14)XR-WmJBVZJ8Z?2GRj@)XksI!k{ed#i|u}>SyW52q-6iu}4^;#=Xa9-bjJ9=)z zgbDfZjhEfQ_Ul-l7lQ)*QuQI4>;dJSWlI9NEKnXThSPF#K}8)Z_OF}k>Xy;>44N*s zb}#Q|2LuW$%hqTzjhvIMV@4z8?Zu%y0|&+|lDS1xs|Qm)o|QdFcaz)UUMH21@Mj0 z8X;bbq?vS9s=dUO`x&shGGx3tx&q~4BGU7MjRnkGc}cFM1cyy_VdEBiD$T8dSk^-U zMJDw1*3)~k%&iI^ETo1~iUNh9JccA6y;p|y_BI}-r_!73ZaXcc9AB|{Ov7KFmUbS? zg=*97i|U?x$l9!`*b_^rhw?B~PRE9ziVp7xeG_Z(RNd04eSA{I*vzvx1ob?g)twLd z7C~f}RQnjqh0hf(fg3}4_@~ytrl}Q^0<&DhD#H()`Kr*1bL~@$P>r2L%ogB_(nW^6Dd8~Z>lC*^b^rPS~5ex#XvuN zg}Z}apfup~NZ_I0n%;I^*BjFV=E#d030qWJ55{ZAj@&+Mr@JpN z2Kde?OT9A;BeIA>c@&l8l;v{JmO1~0jx(C{HQ{R%eWB`KTKFg(2QsAK{>!uLYJQ4$ znK`cPF=B)&C7?Y1BI8=!@J3{wt|_x9Q%Iww3`_cnsZ){Xc)wQX?d%9W4yn;CLN`pT zq}c4tPiUq?dBy>G-SK^_{QcjAqg;8D-r;we#y z54+vc#-TimsW?JFUz-qApc28+TuMLYDR?n()U_t%xLlAZx;DGkRk|;k_)xd$(q>W+#;HX0A+@P4My$6WW)aPqv6c_Td&jP2#H)4=GO$=)l`?{@ql1 zP@YaT(BC8QN$gNZ!*E!zTB0GB+qKiIo6%kd`Cip^j}ueVJf4=u!*`SvrC!W%BNm`M ztX|?dVfUs_WUAMhPLgBdw@&jso+CJY=Kzi_;r(Ox1=lCCLWpvy6U;}LZf}m~LV4=` zfg1%^eN#iy8J0$ z;;P$57n8X~Ed!(GUnmsZwM>yn5pNmOB$!F&M@6J)DygTWzV$noQkeboJ@jiBtkp4N z zTE5by*X1N}hf62FG!KRiD-}Ms>MK!@j1W{JzhXH|!j>)j8U|=_>t6nXT_~3pqs$tO*Tb=g6A3UP_sFcm-JBYlQCOK+h(rB&=fVTN&& zTr&CSk_N5XS-kzD3xbDNo;~;&FI2Fl!&Z6lU7AmXhMSs>JBsmeEtF@{vN~WyY|BKS zG>hda&+)-)_m`!(hmYi$Rdk77Wx992I@3H4=b<$_z~yHyf=-6=%o!d5VyCVmiLO)D zu}))MkE4B?wS?8q9Z;KFEO(IL5E!+}V`p)-`9hbEPou(Kg7P#Gxv?fU=c_FB13ES{ zy7x^_mLB&oy58u~Zy>u<=eOOa*sYzyQL}{KmOwYa?e;-=jN_~{=~%TfF=(2w%A>@m zFfM&EG5=ytQqn5}WNNugAY-XQXn5U;gH%6Xdpgh(GA5WascZgec4dUscHxta`n@3}389V#ebC(Sq{SGY@udO5CNfiawD_ESDkD zQ+EF0)V}e*#@E{pJKRM*3iN6!sVZiJ^6Vx_Iy-fl_O)ypoSy9H0MAEj@J*)^Qo)0DCeW>AFksEKO?|El0X-nwH&ugdg&;$~L~mmlErZf)$W2kYs4 zimi0a|Gb2aB@K*ivAJyvL3vE>loSrNz36i@xGUK4i1o&DrF;oN5~Uu~n9h5@xQvFG zk-sOpVO!7{$O>gDm*$R2DF3oV)rqDZ_&`=~p# zCPvraxAp=TlxI)7|DnLWD_OL)$kyZV3~Pup)vdl^c{t=oQtyzeasskP0Ov_7}bBlew^=FKSAN22-p{*zC(-Y$b)@ZPWwuQ1pps zk9pggp*+(R!O_gGz%in7urNW$*wxx=U?7X9k>ei8>A^&liLSK4IwMoMo#org2$UU? z5DDdJIgfNaMT8_gG#w**a0RzEhE|i@o~p8LuPMlvnh0!N@spDZyk!?S>UAe{>sV|B zlxO;4s7F0o+t21ZjAp&etDL3CkM?;}O|RCfx>*o=GGsE~!^m;>8GAa5&xLe_67K*W zT#JJ1C~)0M2MAn~{j&~wpt9gFS~v^?hrxlb4nAHu41s{b!AyaGA;648z$n0sLcow< zMk8P-FjIp4|L#lmqYU)t+k?Y!fD6F90LTLzkUZaAI-n157QjILGT^}J00aHYKmg|j z3`)=dCI?;w)z8&6kbxlp3z!j50&P%8IE)_b3z%RT3zmUQ1Lp7TzuTz4w*lxs`v3G0 z|Kd^ni-!a~Fu=Ki0pHu(`wV1oLjb4&m-OSNf3N;JfV2A%U^`d?d2sUpjGrF(;s8DH gVS$Sv@Q?JzxWD^=95o;;AaL{I`z{8M!7;w;zXOXAasU7T literal 0 HcmV?d00001 diff --git a/proofs/solidity-verifier/src/trace_replay.rs b/proofs/solidity-verifier/src/trace_replay.rs new file mode 100644 index 000000000..4d270d740 --- /dev/null +++ b/proofs/solidity-verifier/src/trace_replay.rs @@ -0,0 +1,296 @@ +//! Generic Fiat-Shamir replay helper used by the cross-trace tests +//! (poseidon, IVC). +//! +//! Given a midnight-proofs `VerifyingKey` and a Keccak256-transcript +//! proof produced for it, this walks the transcript exactly the way +//! `midnight-proofs::plonk::verify` would — common-input the VK +//! digest, common-input the committed instance commitment(s) and the +//! non-committed instance, then loop through phases / lookups / +//! permutation / trashcans / quotient limbs, then read all +//! evaluations, and finally drive the KZG multi-open transcript +//! sequence (`x1`, `x2`, `f_com`, `x3`, `q_evals`, `x4`, `pi`) — and +//! records every challenge / scalar read / point read into a +//! [`Trace`]. +//! +//! The function is **CS-driven**, not circuit-specific: it walks the +//! `ConstraintSystem` exposed by the VK so the same code handles +//! `numLookups == 1` (poseidon, RSA) and `numLookups == 2` (IVC) and +//! any future circuit shape that fits the §7.2 invariants in +//! `ARCHITECTURE.md`. +//! +//! Used by: +//! +//! * `tests/forge.rs::rust_and_solidity_traces_match` — poseidon end-to-end +//! transcript-level equivalence. +//! * `tests/ivc.rs::rust_and_solidity_ivc_traces_match` — IVC +//! final-step Keccak256 transcript-level equivalence. + +use ff::PrimeField; +use group::Group; +use midnight_curves::{Bls12, Fq, G1Projective}; +#[cfg(feature = "fewer-point-sets")] +use midnight_proofs::poly::Rotation; +use midnight_proofs::{plonk::VerifyingKey, poly::kzg::KZGCommitmentScheme}; + +use crate::{eip2537, trace::Trace, transcript::TracingTranscript}; + +/// Replay the verifier's Keccak256 Fiat-Shamir sequence and record +/// every transcript-observable event in a [`Trace`]. +/// +/// `nb_committed_instances` is the number of committed-instance +/// commitments the prover absorbed before the non-committed instance +/// columns. +pub fn replay_trace_from_vk( + vk: &VerifyingKey>, + proof: &[u8], + public_inputs: &[Fq], + nb_committed_instances: usize, +) -> Trace { + let cs = vk.cs(); + let mut t = TracingTranscript::init_from_bytes(proof); + + // --- hash VK digest into transcript ---------------------------------- + t.common_fq(&vk.transcript_repr()).unwrap(); + t.trace + .borrow_mut() + .intermediate("vk_repr", eip2537::fq_to_be_hex(&vk.transcript_repr())); + + // --- hash committed instance commitments (G1::identity() in tree) --- + let committed_identity: G1Projective = ::identity(); + for _ in 0..nb_committed_instances { + t.common_g1(&committed_identity).unwrap(); + } + + // --- hash non-committed instances ----------------------------------- + let normal_instance_columns = cs.num_instance_columns() - nb_committed_instances; + for column in 0..normal_instance_columns { + let values = if normal_instance_columns == 1 || column + 1 == normal_instance_columns { + public_inputs + } else { + &[] + }; + let inst_len = Fq::from_u128(values.len() as u128); + t.common_fq(&inst_len).unwrap(); + for v in values { + t.common_fq(v).unwrap(); + } + } + + // --- phases: read advice + squeeze advice challenges ---------------- + for (phase_idx, _phase) in cs.phases().enumerate() { + for i in 0..cs.num_advice_columns() { + let _ = t + .read_g1(&format!("advice[{i}]")) + .unwrap_or_else(|e| panic!("read advice[{i}] phase {phase_idx}: {e}")); + } + for _ in 0..cs.num_challenges() { + let _ = t.squeeze_fq("advice_challenge"); + } + } + let _theta = t.squeeze_fq("theta"); + + // --- lookup multiplicities ------------------------------------------ + for _ in 0..cs.lookups().len() { + let _ = t.read_g1("lookup_mult").unwrap(); + } + + let _beta = t.squeeze_fq("beta"); + let _gamma = t.squeeze_fq("gamma"); + + // --- permutation products (one G1 per chunk) ------------------------- + let perm_chunks = { + let chunk_len = cs.degree() - 2; + let cols = cs.permutation().get_columns().len(); + if cols == 0 { + 0 + } else { + cols.div_ceil(chunk_len) + } + }; + for _ in 0..perm_chunks { + let _ = t.read_g1("perm_product").unwrap(); + } + + // --- lookup commitments: per lookup, helpers (nChunks) + accumulator + for l in cs.lookups().iter() { + let nc = l.chunk_by_degree(cs.degree()).num_chunks(); + for _ in 0..nc { + let _ = t.read_g1("lookup_helper").unwrap(); + } + let _ = t.read_g1("lookup_acc").unwrap(); + } + + let _trash_ch = t.squeeze_fq("trash_challenge"); + for _ in 0..cs.trashcans().len() { + let _ = t.read_g1("trashcan").unwrap(); + } + let _y = t.squeeze_fq("y"); + + // --- quotient limbs --------------------------------------------------- + let nb_q = vk.get_domain().get_quotient_poly_degree(); + for _ in 0..nb_q { + let _ = t.read_g1("quotient_limb").unwrap(); + } + + let _x = t.squeeze_fq("x"); + + // --- read all evaluation scalars in Rust iterator order -------------- + let num_fixed_q = cs + .fixed_queries() + .iter() + .filter(|(c, _)| !cs.has_simple_selector_col(c.index())) + .count(); + // For committed-instance columns the verifier *reads* the eval + // from the transcript; for non-committed columns it computes the + // eval locally via Lagrange interpolation, so no transcript read + // is performed there. `nb_committed_instances` is the column + // index cutoff: instance queries on columns `< nb` are + // transcript reads. + let num_committed_instance_reads = cs + .instance_queries() + .iter() + .filter(|(col, _)| col.index() < nb_committed_instances) + .count(); + for _ in 0..num_committed_instance_reads { + let _ = t.read_fq("committed_instance_eval").unwrap(); + } + + for i in 0..cs.advice_queries().len() { + let _ = t.read_fq(&format!("advice_eval[{i}]")).unwrap(); + } + for i in 0..num_fixed_q { + let _ = t.read_fq(&format!("fixed_eval[{i}]")).unwrap(); + } + for i in 0..cs.permutation().get_columns().len() { + let _ = t.read_fq(&format!("perm_common[{i}]")).unwrap(); + } + for i in 0..perm_chunks { + let _ = t.read_fq("perm_cur").unwrap(); + let _ = t.read_fq("perm_next").unwrap(); + if i + 1 != perm_chunks { + let _ = t.read_fq("perm_last").unwrap(); + } + } + for l in cs.lookups().iter() { + let nc = l.chunk_by_degree(cs.degree()).num_chunks(); + let _ = t.read_fq("lookup_m_eval").unwrap(); + for _ in 0..nc { + let _ = t.read_fq("lookup_helper_eval").unwrap(); + } + let _ = t.read_fq("lookup_acc_eval").unwrap(); + let _ = t.read_fq("lookup_acc_next_eval").unwrap(); + } + for _ in 0..cs.trashcans().len() { + let _ = t.read_fq("trash_eval").unwrap(); + } + + // --- KZG multi-open -------------------------------------------------- + #[cfg(feature = "fewer-point-sets")] + { + let dummy_count = dummy_query_count(vk, _x, nb_committed_instances); + for _ in 0..dummy_count { + let _ = t.read_fq("dummy_eval").unwrap(); + } + } + let _x1 = t.squeeze_fq("x1"); + let _x2 = t.squeeze_fq("x2"); + let _f_com = t.read_g1("f_com").unwrap(); + let _x3 = t.squeeze_fq("x3"); + // Read remaining q_evals from the proof until only 48 bytes — + // exactly one G1 point — remain. + loop { + let buf = t.inner_mut().buffer(); + let remaining = buf.get_ref().len() as i64 - buf.position() as i64; + if remaining <= 48 { + break; + } + let _ = t.read_fq("q_eval").unwrap(); + } + let _x4 = t.squeeze_fq("x4"); + let _pi = t.read_g1("pi").unwrap(); + + t.trace() +} + +#[cfg(feature = "fewer-point-sets")] +fn dummy_query_count( + vk: &VerifyingKey>, + x: Fq, + nb_committed_instances: usize, +) -> usize { + let cs = vk.cs(); + let domain = vk.get_domain(); + let chunk_len = cs.degree() - 2; + let perm_chunks = { + let cols = cs.permutation().get_columns().len(); + if cols == 0 { + 0 + } else { + cols.div_ceil(chunk_len) + } + }; + let total_lookup_helpers: usize = + cs.lookups().iter().map(|l| l.chunk_by_degree(cs.degree()).num_chunks()).sum(); + + let advice_base = 0usize; + let instance_base = advice_base + cs.num_advice_columns(); + let perm_prod_base = instance_base + cs.num_instance_columns(); + let lookup_m_base = perm_prod_base + perm_chunks; + let lookup_helper_base = lookup_m_base + cs.lookups().len(); + let lookup_acc_base = lookup_helper_base + total_lookup_helpers; + let trash_base = lookup_acc_base + cs.lookups().len(); + let fixed_base = trash_base + cs.trashcans().len(); + let perm_common_base = fixed_base + cs.num_fixed_columns(); + let lin_base = perm_common_base + cs.permutation().get_columns().len(); + + let mut pairs = Vec::<(usize, Fq)>::new(); + for &(column, at) in cs.advice_queries() { + pairs.push((advice_base + column.index(), domain.rotate_omega(x, at))); + } + for &(column, at) in cs.instance_queries() { + if column.index() < nb_committed_instances { + pairs.push((instance_base + column.index(), domain.rotate_omega(x, at))); + } + } + + let x_next = domain.rotate_omega(x, Rotation::next()); + let x_last = domain.rotate_omega(x, Rotation(-((cs.blinding_factors() + 1) as i32))); + for i in 0..perm_chunks { + pairs.push((perm_prod_base + i, x)); + pairs.push((perm_prod_base + i, x_next)); + } + if perm_chunks > 1 { + for i in (0..perm_chunks - 1).rev() { + pairs.push((perm_prod_base + i, x_last)); + } + } + + let mut helper_offset = 0usize; + for (l_idx, lookup) in cs.lookups().iter().enumerate() { + pairs.push((lookup_m_base + l_idx, x)); + let chunks = lookup.chunk_by_degree(cs.degree()).num_chunks(); + for j in 0..chunks { + pairs.push((lookup_helper_base + helper_offset + j, x)); + } + helper_offset += chunks; + pairs.push((lookup_acc_base + l_idx, x)); + pairs.push((lookup_acc_base + l_idx, x_next)); + } + + for i in 0..cs.trashcans().len() { + pairs.push((trash_base + i, x)); + } + for &(column, at) in cs + .fixed_queries() + .iter() + .filter(|(col, _)| !cs.has_simple_selector_col(col.index())) + { + pairs.push((fixed_base + column.index(), domain.rotate_omega(x, at))); + } + for i in 0..cs.permutation().get_columns().len() { + pairs.push((perm_common_base + i, x)); + } + pairs.push((lin_base, x)); + + midnight_proofs::poly::kzg::compute_dummy_queries(&pairs).len() +} diff --git a/proofs/src/transcript/implementors.rs b/proofs/src/transcript/implementors.rs index d4e6b5a74..689c10a81 100644 --- a/proofs/src/transcript/implementors.rs +++ b/proofs/src/transcript/implementors.rs @@ -2,7 +2,7 @@ use std::{io, io::Read}; use blake2b_simd::{Params, State as Blake2bState}; use ff::{FromUniformBytes, PrimeField}; -use group::GroupEncoding; +use group::{prime::PrimeCurveAffine, GroupEncoding, UncompressedEncoding}; #[cfg(feature = "dev-curves")] use midnight_curves::bn256::{Fr, G1}; #[cfg(feature = "keccak-transcript")] @@ -262,8 +262,40 @@ impl Sampleable for Fr { #[cfg(feature = "keccak-transcript")] impl Hashable for midnight_curves::G1Projective { + /// Converts the point to the EIP-2537 padded uncompressed form for + /// Fiat-Shamir absorbtion: 128 bytes laid out as + /// + /// x_hi (32) || x_lo (32) || y_hi (32) || y_lo (32) + /// + /// where each coord is 16 zero pad bytes followed by 48 big-endian + /// bytes of the BLS12-381 base-field element. The identity point + /// is encoded as 128 zero bytes (matches the EIP-2537 (0, 0) + /// convention rather than the BLS-spec 0x40-leading-byte form). + /// + /// This format lets the EVM verifier (`Halo2Verifier.sol:: + /// common_g1_uncompressed`) skip the 384-bit sign-bit ladder it + /// would need if we were hashing the 48-byte compressed encoding; + /// the EVM can just memcpy the calldata words verbatim into the + /// keccak buffer (after masking the 16-byte zero pads to defeat + /// transcript malleability). + /// + /// The wire format (`to_bytes`) is unchanged — points are still + /// transmitted in the 48-byte compressed encoding. fn to_input(&self) -> Vec { - Hashable::::to_bytes(self) + let aff = midnight_curves::G1Affine::from(self); + let mut out = vec![0u8; 128]; + if !bool::from(aff.is_identity()) { + // `UncompressedEncoding::to_uncompressed` returns a wrapper + // around 96 bytes: x_be(48) || y_be(48). For non-identity + // points, the top 3 bits of byte 0 are zero (since + // x < p < 2^381), so we copy the bytes verbatim into the + // EIP-2537 64-byte slots. + let raw = ::to_uncompressed(&aff); + let bytes: &[u8] = raw.as_ref(); + out[16..64].copy_from_slice(&bytes[0..48]); + out[80..128].copy_from_slice(&bytes[48..96]); + } + out } fn to_bytes(&self) -> Vec { diff --git a/zk_stdlib/Cargo.toml b/zk_stdlib/Cargo.toml index 577361b23..8d720865c 100644 --- a/zk_stdlib/Cargo.toml +++ b/zk_stdlib/Cargo.toml @@ -16,6 +16,7 @@ midnight-curves = { version = "0.2.0" } midnight-proofs = { version = "0.7.0", default-features = false, features = [ "circuit-params", "committed-instances", + "keccak-transcript", ] } keccak_sha3 = { package = "sha3-circuit", git = "https://github.com/alexandroszacharakis8/sha3-circuit", rev = "1a3ef7d2"} @@ -23,6 +24,7 @@ blake2b = { package = "blake2b_halo2", git = "https://github.com/eryxcoop/blake2 serde = { workspace = true, optional = true } sha2 = { workspace = true } +sha3 = { workspace = true } rand = { workspace = true } rayon = { workspace = true } @@ -42,7 +44,6 @@ itertools = "0.14" dhat = "0.3" bellman = "0.14.0" serial_test = "3.2.0" -sha3 = "0.10.0" blake2 = "0.10.6" [target.'cfg(ci_build)'.dependencies] diff --git a/zk_stdlib/examples/poseidon.rs b/zk_stdlib/examples/poseidon.rs index 3b66a9e62..ea8716a41 100644 --- a/zk_stdlib/examples/poseidon.rs +++ b/zk_stdlib/examples/poseidon.rs @@ -12,6 +12,7 @@ use midnight_proofs::{ use midnight_zk_stdlib::{utils::plonk_api::srs_for_test, Relation, ZkStdLib, ZkStdLibArch}; use rand::{rngs::OsRng, SeedableRng}; use rand_chacha::ChaCha8Rng; +use sha3::Keccak256; type F = midnight_curves::Fq; @@ -66,13 +67,14 @@ fn main() { let witness: [F; 3] = core::array::from_fn(|_| F::random(&mut rng)); let instance = as HashCPU>::hash(&witness); - let proof = midnight_zk_stdlib::prove::( + let proof = midnight_zk_stdlib::prove::( &srs, &pk, &relation, &instance, witness, OsRng, ) .expect("Proof generation should not fail"); + assert!( - midnight_zk_stdlib::verify::( + midnight_zk_stdlib::verify::( &srs.verifier_params(), &vk, &instance, From 6220680ca2b1918406d0ccf034cef671cbb5dabe Mon Sep 17 00:00:00 2001 From: Julien Coolen Date: Fri, 15 May 2026 19:18:14 +0200 Subject: [PATCH 03/19] feat(kzg): split fewer point set controls Separate verifier options for keeping instance and fold point sets so aggregation and circuit paths can tune verifier MSM length independently. --- aggregation/Cargo.toml | 7 +- circuits/Cargo.toml | 12 ++- circuits/src/verifier/kzg.rs | 2 +- proofs/src/poly/kzg/mod.rs | 138 +++++++++++++++++++++++++++++------ proofs/src/poly/kzg/utils.rs | 1 - 5 files changed, 130 insertions(+), 30 deletions(-) diff --git a/aggregation/Cargo.toml b/aggregation/Cargo.toml index 8e4eaae38..78d35c777 100644 --- a/aggregation/Cargo.toml +++ b/aggregation/Cargo.toml @@ -51,8 +51,13 @@ single-h-commitment = [ "midnight-circuits/single-h-commitment", "midnight-zk-stdlib/single-h-commitment", ] -fewer-point-sets = [ +proof-fewer-point-sets = ["midnight-proofs/fewer-point-sets"] +in-circuit-fewer-point-sets = [ "midnight-proofs/fewer-point-sets", + "midnight-circuits/in-circuit-fewer-point-sets", +] +fewer-point-sets = [ + "proof-fewer-point-sets", "midnight-circuits/fewer-point-sets", ] # Enables `IvcProver::prove_final_step` / `IvcVerifier::verify_final`, diff --git a/circuits/Cargo.toml b/circuits/Cargo.toml index bf3426d1e..f4535b7e8 100644 --- a/circuits/Cargo.toml +++ b/circuits/Cargo.toml @@ -55,9 +55,15 @@ heap_profiling = [] truncated-challenges = ["midnight-proofs/truncated-challenges"] # Commit to the quotient polynomial H as a single commitment (see midnight-proofs) single-h-commitment = ["midnight-proofs/single-h-commitment"] -# Enables a heuristic to reduce the number of distinct multi-open point sets, -# lowering the verifier MSM length (see midnight-proofs for details). -fewer-point-sets = ["midnight-proofs/fewer-point-sets"] +# Enables the in-circuit verifier dummy-query heuristic, lowering the verifier +# MSM length for proofs checked inside a circuit. +in-circuit-fewer-point-sets = [] +# Backwards-compatible feature: enable both the in-circuit verifier heuristic +# and the proof-system dummy-query capability. +fewer-point-sets = [ + "in-circuit-fewer-point-sets", + "midnight-proofs/fewer-point-sets", +] # Enable development curves (BN256, Pasta) dev-curves = ["midnight-curves/dev-curves", "midnight-proofs/dev-curves"] diff --git a/circuits/src/verifier/kzg.rs b/circuits/src/verifier/kzg.rs index ac14450de..dd8dbc808 100644 --- a/circuits/src/verifier/kzg.rs +++ b/circuits/src/verifier/kzg.rs @@ -330,7 +330,7 @@ pub(crate) fn multi_prepare( queries: &[VerifierQuery], ) -> Result, Error> { // Add dummy queries to reduce the number of distinct multi-open point sets. - #[cfg(feature = "fewer-point-sets")] + #[cfg(any(feature = "fewer-point-sets", feature = "in-circuit-fewer-point-sets"))] let queries = &{ let pairs: Vec<_> = queries.iter().map(|q| (q.get_commitment(), q.get_point())).collect(); let dummy_openings = midnight_proofs::poly::kzg::compute_dummy_queries(&pairs); diff --git a/proofs/src/poly/kzg/mod.rs b/proofs/src/poly/kzg/mod.rs index 66d3be8f4..a43d76fea 100644 --- a/proofs/src/poly/kzg/mod.rs +++ b/proofs/src/poly/kzg/mod.rs @@ -26,9 +26,84 @@ use ff::Field; use group::Group; use midnight_curves::pairing::MultiMillerLoop; use rand_core::OsRng; -#[cfg(feature = "fewer-point-sets")] pub use utils::compute_dummy_queries; +#[cfg(feature = "fewer-point-sets")] +mod fewer_point_sets_runtime { + use std::cell::Cell; + + thread_local! { + static ENABLED: Cell = const { Cell::new(true) }; + } + + pub fn enabled() -> bool { + ENABLED.with(Cell::get) + } + + /// Guard that restores the previous fewer-point-sets runtime setting when dropped. + #[derive(Debug)] + pub struct ScopedFewerPointSets { + previous: bool, + } + + pub fn scoped(enabled: bool) -> ScopedFewerPointSets { + let previous = ENABLED.with(|cell| { + let previous = cell.get(); + cell.set(enabled); + previous + }); + ScopedFewerPointSets { previous } + } + + impl Drop for ScopedFewerPointSets { + fn drop(&mut self) { + ENABLED.with(|cell| cell.set(self.previous)); + } + } +} + +#[cfg(feature = "fewer-point-sets")] +pub use fewer_point_sets_runtime::ScopedFewerPointSets; + +/// No-op guard returned when the proof-system fewer-point-sets capability is +/// not compiled in. +#[cfg(not(feature = "fewer-point-sets"))] +#[derive(Debug)] +pub struct ScopedFewerPointSets; + +/// Returns whether KZG multi-open dummy queries are currently enabled. +/// +/// With the `fewer-point-sets` feature compiled in, the default is `true` to +/// preserve the historical feature behavior. Call [`scoped_fewer_point_sets`] +/// to temporarily override it for a specific proof. +pub fn fewer_point_sets_enabled() -> bool { + #[cfg(feature = "fewer-point-sets")] + { + fewer_point_sets_runtime::enabled() + } + #[cfg(not(feature = "fewer-point-sets"))] + { + false + } +} + +/// Temporarily enables or disables KZG multi-open dummy queries on this thread. +/// +/// This is intentionally scoped so recursive proving can use fewer point sets +/// for proofs verified inside a circuit while an outer proof in the same process +/// can be emitted without dummy query scalars. +pub fn scoped_fewer_point_sets(enabled: bool) -> ScopedFewerPointSets { + #[cfg(feature = "fewer-point-sets")] + { + fewer_point_sets_runtime::scoped(enabled) + } + #[cfg(not(feature = "fewer-point-sets"))] + { + let _ = enabled; + ScopedFewerPointSets + } +} + #[cfg(feature = "truncated-challenges")] use crate::utils::arithmetic::{truncate, truncated_powers}; use crate::{ @@ -120,18 +195,25 @@ where .unwrap() } - // Add dummy queries to reduce the number of distinct multi-open point sets. #[cfg(feature = "fewer-point-sets")] - let queries = &{ - let mut queries = queries.to_vec(); - let pairs: Vec<_> = queries.iter().map(|q| (q.get_commitment(), q.point)).collect(); - for (idx, point) in compute_dummy_queries(&pairs) { - let poly = queries[idx].poly; - transcript - .write(&eval_polynomial(poly, point)) - .map_err(|_| Error::OpeningError)?; - queries.push(ProverQuery::new(point, poly)); - } + let queries_with_dummies; + #[cfg(feature = "fewer-point-sets")] + let queries = if fewer_point_sets_enabled() { + // Add dummy queries to reduce the number of distinct multi-open point sets. + queries_with_dummies = { + let mut queries = queries.to_vec(); + let pairs: Vec<_> = queries.iter().map(|q| (q.get_commitment(), q.point)).collect(); + for (idx, point) in compute_dummy_queries(&pairs) { + let poly = queries[idx].poly; + transcript + .write(&eval_polynomial(poly, point)) + .map_err(|_| Error::OpeningError)?; + queries.push(ProverQuery::new(point, poly)); + } + queries + }; + &queries_with_dummies + } else { queries }; @@ -241,19 +323,27 @@ where E::Fr: Sampleable + Ord + Hash + Hashable, E::G1: 'com + Hashable + CurveExt, { - // Add dummy queries to reduce the number of distinct multi-open point sets. #[cfg(feature = "fewer-point-sets")] - let queries = &{ - let mut queries = queries.to_vec(); - let pairs: Vec<_> = queries.iter().map(|q| (q.commitment.clone(), q.point)).collect(); - for (idx, point) in compute_dummy_queries(&pairs) { - queries.push(VerifierQuery { - point, - commitment_label: queries[idx].commitment_label.clone(), - commitment: queries[idx].commitment.clone(), - eval: transcript.read().map_err(|_| Error::SamplingError)?, - }); - } + let queries_with_dummies; + #[cfg(feature = "fewer-point-sets")] + let queries = if fewer_point_sets_enabled() { + // Add dummy queries to reduce the number of distinct multi-open point sets. + queries_with_dummies = { + let mut queries = queries.to_vec(); + let pairs: Vec<_> = + queries.iter().map(|q| (q.commitment.clone(), q.point)).collect(); + for (idx, point) in compute_dummy_queries(&pairs) { + queries.push(VerifierQuery { + point, + commitment_label: queries[idx].commitment_label.clone(), + commitment: queries[idx].commitment.clone(), + eval: transcript.read().map_err(|_| Error::SamplingError)?, + }); + } + queries + }; + &queries_with_dummies + } else { queries }; diff --git a/proofs/src/poly/kzg/utils.rs b/proofs/src/poly/kzg/utils.rs index e6b3d125f..5164df215 100644 --- a/proofs/src/poly/kzg/utils.rs +++ b/proofs/src/poly/kzg/utils.rs @@ -147,7 +147,6 @@ pub fn construct_intermediate_sets>( /// /// The output order is deterministic (insertion order), so prover and verifier /// stay in sync. -#[cfg(feature = "fewer-point-sets")] pub fn compute_dummy_queries( pairs: &[(K, P)], ) -> Vec<(usize, P)> { From db268277f8b288f3f6a5de21c92d136f1a92bbd1 Mon Sep 17 00:00:00 2001 From: Julien Coolen Date: Fri, 15 May 2026 19:18:19 +0200 Subject: [PATCH 04/19] test(solidity): add native differential trace hooks Expose native verifier trace points across PLONK, KZG, circuit accumulator checks, and transcript wiring so Solidity verifier replay can compare intermediate values. --- circuits/Cargo.toml | 6 + circuits/src/verifier/accumulator.rs | 31 +++- proofs/Cargo.toml | 7 + proofs/src/plonk/logup/verifier.rs | 49 +++++ proofs/src/plonk/mod.rs | 2 + proofs/src/plonk/permutation/verifier.rs | 42 ++++- proofs/src/plonk/solidity_trace.rs | 140 ++++++++++++++ proofs/src/plonk/trash/verifier.rs | 14 ++ proofs/src/plonk/verifier.rs | 222 ++++++++++++++++++++++- proofs/src/poly/commitment.rs | 4 +- proofs/src/poly/kzg/mod.rs | 79 +++++++- proofs/src/poly/kzg/msm.rs | 10 +- proofs/src/transcript/mod.rs | 27 +++ zk_stdlib/src/lib.rs | 7 +- zk_stdlib/src/utils/plonk_api.rs | 6 +- 15 files changed, 633 insertions(+), 13 deletions(-) create mode 100644 proofs/src/plonk/solidity_trace.rs diff --git a/circuits/Cargo.toml b/circuits/Cargo.toml index f4535b7e8..85464a9b0 100644 --- a/circuits/Cargo.toml +++ b/circuits/Cargo.toml @@ -20,6 +20,7 @@ midnight-proofs = { version = "0.7.0", default-features = false, features = [ serde = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } sha2 = { workspace = true } +sha3 = { workspace = true, optional = true } ripemd = { workspace = true } rand = { workspace = true } @@ -66,6 +67,11 @@ fewer-point-sets = [ ] # Enable development curves (BN256, Pasta) dev-curves = ["midnight-curves/dev-curves", "midnight-proofs/dev-curves"] +solidity-verifier-trace = [ + "dep:sha3", + "midnight-proofs/keccak-transcript", + "midnight-proofs/solidity-verifier-trace", +] [lib] bench = false diff --git a/circuits/src/verifier/accumulator.rs b/circuits/src/verifier/accumulator.rs index e0949f412..5fd986a76 100644 --- a/circuits/src/verifier/accumulator.rs +++ b/circuits/src/verifier/accumulator.rs @@ -135,13 +135,40 @@ impl Accumulator { /// Checks whether the accumulator, when evaluated with the provided /// fixed-bases, satisfies the pairing invariant w.r.t. the SRS verifier /// parameters. + #[cfg(not(feature = "solidity-verifier-trace"))] pub fn check( &self, params: &ParamsVerifierKZG, fixed_bases: &BTreeMap, ) -> bool { - let lhs = MSMKZG::::from_base(&self.lhs.eval(fixed_bases)); - let rhs = MSMKZG::::from_base(&self.rhs.eval(fixed_bases)); + let lhs_eval = self.lhs.eval(fixed_bases); + let rhs_eval = self.rhs.eval(fixed_bases); + let lhs = MSMKZG::::from_base(&lhs_eval); + let rhs = MSMKZG::::from_base(&rhs_eval); + DualMSM::new(lhs, rhs).check(params) + } + + /// Checks whether the accumulator satisfies the pairing invariant and + /// records the evaluated sides for Solidity trace differentials. + #[cfg(feature = "solidity-verifier-trace")] + pub fn check( + &self, + params: &ParamsVerifierKZG, + fixed_bases: &BTreeMap, + ) -> bool + where + S::C: midnight_proofs::transcript::Hashable, + { + let lhs_eval = self.lhs.eval(fixed_bases); + let rhs_eval = self.rhs.eval(fixed_bases); + midnight_proofs::plonk::solidity_trace::record_hashable::( + 29, "acc_lhs", &lhs_eval, + ); + midnight_proofs::plonk::solidity_trace::record_hashable::( + 30, "acc_rhs", &rhs_eval, + ); + let lhs = MSMKZG::::from_base(&lhs_eval); + let rhs = MSMKZG::::from_base(&rhs_eval); DualMSM::new(lhs, rhs).check(params) } diff --git a/proofs/Cargo.toml b/proofs/Cargo.toml index 3933975de..ce4207c10 100644 --- a/proofs/Cargo.toml +++ b/proofs/Cargo.toml @@ -109,5 +109,12 @@ single-h-commitment = [] # polynomials, which may slightly increase proof size. fewer-point-sets = [] +# Test/debug-only instrumentation used by the Solidity verifier generator to +# differentially compare the native Rust verifier against generated Solidity +# trace logs. The hooks record transcript-compatible scalar/G1 bytes and +# selected verifier intermediate values, but are inert unless explicitly +# started by the caller. +solidity-verifier-trace = [] + [lib] bench = false diff --git a/proofs/src/plonk/logup/verifier.rs b/proofs/src/plonk/logup/verifier.rs index a2070ad26..9df927de9 100644 --- a/proofs/src/plonk/logup/verifier.rs +++ b/proofs/src/plonk/logup/verifier.rs @@ -56,8 +56,15 @@ impl> ChunkedArgument { ) -> Result, Error> where CS::Commitment: Hashable, + ::Input: + crate::transcript::TranscriptInputBytes, { let multiplicities = transcript.read()?; + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_proof_commitment::( + "proof_lookup_multiplicity_commitment", + &multiplicities, + ); Ok(CommittedMultiplicities { multiplicities }) } } @@ -74,10 +81,26 @@ impl, CS: PolynomialCommitmentScheme> ) -> Result, Error> where CS::Commitment: Hashable, + ::Input: + crate::transcript::TranscriptInputBytes, { let helper_polys = (0..nb_chunks).map(|_| transcript.read()).collect::, _>>()?; + #[cfg(feature = "solidity-verifier-trace")] + { + for commitment in &helper_polys { + crate::plonk::solidity_trace::record_proof_commitment::( + "proof_lookup_helper_commitment", + commitment, + ); + } + } let accumulator = transcript.read()?; + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_proof_commitment::( + "proof_lookup_accumulator_commitment", + &accumulator, + ); Ok(Committed { multiplicities: self.multiplicities, @@ -98,14 +121,40 @@ impl> Committed { ) -> Result, Error> where F: Hashable, + ::Input: + crate::transcript::TranscriptInputBytes, { let nb_chunks = self.helper_polys.len(); let multiplicities_eval = transcript.read()?; + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_proof_eval::( + "proof_lookup_multiplicity_eval", + &multiplicities_eval, + ); let helper_evals = (0..nb_chunks).map(|_| transcript.read()).collect::, _>>()?; + #[cfg(feature = "solidity-verifier-trace")] + { + for eval in &helper_evals { + crate::plonk::solidity_trace::record_proof_eval::( + "proof_lookup_helper_eval", + eval, + ); + } + } let accumulator_eval = transcript.read()?; + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_proof_eval::( + "proof_lookup_accumulator_eval", + &accumulator_eval, + ); let accumulator_next_eval = transcript.read()?; + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_proof_eval::( + "proof_lookup_accumulator_next_eval", + &accumulator_next_eval, + ); Ok(Evaluated { committed: self, diff --git a/proofs/src/plonk/mod.rs b/proofs/src/plonk/mod.rs index f2d3f87d4..577fe1a86 100644 --- a/proofs/src/plonk/mod.rs +++ b/proofs/src/plonk/mod.rs @@ -31,6 +31,8 @@ mod keygen; pub(crate) mod linearization; pub(crate) mod logup; pub mod permutation; +#[cfg(feature = "solidity-verifier-trace")] +pub mod solidity_trace; pub(crate) mod traces; pub(crate) mod trash; diff --git a/proofs/src/plonk/permutation/verifier.rs b/proofs/src/plonk/permutation/verifier.rs index d0a310c5a..222699532 100644 --- a/proofs/src/plonk/permutation/verifier.rs +++ b/proofs/src/plonk/permutation/verifier.rs @@ -35,6 +35,8 @@ impl Argument { ) -> Result, Error> where CS::Commitment: Hashable, + ::Input: + crate::transcript::TranscriptInputBytes, { let chunk_len = vk.cs_degree - 2; @@ -43,6 +45,15 @@ impl Argument { .chunks(chunk_len) .map(|_| transcript.read()) .collect::, _>>()?; + #[cfg(feature = "solidity-verifier-trace")] + { + for commitment in &permutation_product_commitments { + crate::plonk::solidity_trace::record_proof_commitment::( + "proof_permutation_product_commitment", + commitment, + ); + } + } Ok(Committed { permutation_product_commitments, @@ -57,12 +68,23 @@ impl> VerifyingKey { ) -> Result, Error> where F: Hashable, + ::Input: + crate::transcript::TranscriptInputBytes, { let permutation_evals = self .commitments .iter() .map(|_| transcript.read()) .collect::, _>>()?; + #[cfg(feature = "solidity-verifier-trace")] + { + for eval in &permutation_evals { + crate::plonk::solidity_trace::record_proof_eval::( + "proof_permutation_common_eval", + eval, + ); + } + } Ok(CommonEvaluated { permutation_evals }) } @@ -76,6 +98,8 @@ impl> Committed { where CS::Commitment: Hashable, F: Hashable, + ::Input: + crate::transcript::TranscriptInputBytes, { let mut sets = vec![]; @@ -83,9 +107,25 @@ impl> Committed { while iter.next().is_some() { let permutation_product_eval = transcript.read()?; + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_proof_eval::( + "proof_permutation_product_eval", + &permutation_product_eval, + ); let permutation_product_next_eval = transcript.read()?; + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_proof_eval::( + "proof_permutation_product_next_eval", + &permutation_product_next_eval, + ); let permutation_product_last_eval = if iter.len() > 0 { - Some(transcript.read()?) + let eval = transcript.read()?; + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_proof_eval::( + "proof_permutation_product_last_eval", + &eval, + ); + Some(eval) } else { None }; diff --git a/proofs/src/plonk/solidity_trace.rs b/proofs/src/plonk/solidity_trace.rs new file mode 100644 index 000000000..bc4299c8e --- /dev/null +++ b/proofs/src/plonk/solidity_trace.rs @@ -0,0 +1,140 @@ +//! Trace hooks for differential testing of generated Solidity verifiers. +//! +//! This module is intentionally small and byte-oriented: the Rust verifier +//! records the exact scalar/G1 bytes that correspond to generated Solidity +//! trace logs, and downstream harnesses can compare those bytes without +//! reimplementing verifier algebra. + +use std::cell::RefCell; + +use crate::transcript::{Hashable, TranscriptHash, TranscriptInputBytes}; + +/// First trace topic used for proof G1 commitments, in proof-read order. +pub const PROOF_COMMITMENT_TRACE_BASE: u64 = 10_000; +/// First trace topic used for proof scalar evaluations, in proof-read order. +pub const PROOF_EVAL_TRACE_BASE: u64 = 20_000; +/// First trace topic used for user-phase transcript challenges. +pub const USER_CHALLENGE_TRACE_BASE: u64 = 1_000; +/// First trace topic used for raw quotient identity evaluations. +pub const QUOTIENT_IDENTITY_TRACE_BASE: u64 = 30_000; +/// Trace topic used for the fully grouped quotient numerator `nu_y(x)`. +pub const QUOTIENT_NUMERATOR_TRACE_ID: u64 = 36; +/// First trace topic used for per-point-set PCS `q_com` commitments. +pub const PCS_Q_COM_TRACE_BASE: u64 = 40_000; +/// First trace topic used for serialized PCS point sets. +pub const PCS_POINT_SET_TRACE_BASE: u64 = 41_000; +/// Trace topic used for the materialized linearization commitment. +pub const LINEARIZATION_COMMITMENT_TRACE_ID: u64 = 34; +/// Trace topic used for final selector-fold scalars. +pub const SELECTOR_FOLD_TRACE_BASE: u64 = 60_000; +/// Trace topic used for the final pairing result. +pub const FINAL_RESULT_TRACE_ID: u64 = 35; + +#[derive(Debug, Default)] +struct SolidityTraceState { + events: Vec, + proof_commitments: u64, + proof_evals: u64, +} + +thread_local! { + static EVENTS: RefCell> = const { RefCell::new(None) }; +} + +/// One Rust verifier trace item, keyed by the generated Solidity trace ID. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SolidityTraceEvent { + /// Solidity trace topic ID. + pub id: u64, + /// Stable human-readable name for diagnostics. + pub name: &'static str, + /// Trace payload bytes. Scalars are 32 bytes; G1 points are the + /// transcript's EIP-2537 padded 128-byte representation. + pub data: Vec, +} + +/// Start collecting trace events on the current thread. +pub fn start() { + EVENTS.with(|events| { + *events.borrow_mut() = Some(SolidityTraceState { + events: Vec::new(), + proof_commitments: 0, + proof_evals: 0, + }); + }); +} + +/// Stop collecting and return all events collected on the current thread. +pub fn take() -> Vec { + EVENTS.with(|events| events.borrow_mut().take().map(|state| state.events).unwrap_or_default()) +} + +/// Record an already-rendered byte payload under a Solidity trace topic ID. +pub fn record_bytes(id: u64, name: &'static str, data: impl Into>) { + EVENTS.with(|events| { + if let Some(state) = events.borrow_mut().as_mut() { + state.events.push(SolidityTraceEvent { + id, + name, + data: data.into(), + }); + } + }); +} + +/// Record a `u64` as a 32-byte big-endian Solidity word. +pub fn record_u64(id: u64, name: &'static str, value: u64) { + let mut data = vec![0u8; 32]; + data[24..32].copy_from_slice(&value.to_be_bytes()); + record_bytes(id, name, data); +} + +/// Record a transcript-hashable value using its diagnostic trace bytes. +pub fn record_hashable(id: u64, name: &'static str, value: &T) +where + H: TranscriptHash, + H::Input: TranscriptInputBytes, + T: Hashable, +{ + record_bytes(id, name, value.to_input().into_trace_bytes()); +} + +/// Record the next proof commitment read from the transcript. +pub fn record_proof_commitment(name: &'static str, value: &T) +where + H: TranscriptHash, + H::Input: TranscriptInputBytes, + T: Hashable, +{ + EVENTS.with(|events| { + if let Some(state) = events.borrow_mut().as_mut() { + let id = PROOF_COMMITMENT_TRACE_BASE + state.proof_commitments; + state.proof_commitments += 1; + state.events.push(SolidityTraceEvent { + id, + name, + data: value.to_input().into_trace_bytes(), + }); + } + }); +} + +/// Record the next proof scalar evaluation read from the transcript. +pub fn record_proof_eval(name: &'static str, value: &T) +where + H: TranscriptHash, + H::Input: TranscriptInputBytes, + T: Hashable, +{ + EVENTS.with(|events| { + if let Some(state) = events.borrow_mut().as_mut() { + let id = PROOF_EVAL_TRACE_BASE + state.proof_evals; + state.proof_evals += 1; + state.events.push(SolidityTraceEvent { + id, + name, + data: value.to_input().into_trace_bytes(), + }); + } + }); +} diff --git a/proofs/src/plonk/trash/verifier.rs b/proofs/src/plonk/trash/verifier.rs index e108c3df3..49e87ab6f 100644 --- a/proofs/src/plonk/trash/verifier.rs +++ b/proofs/src/plonk/trash/verifier.rs @@ -25,8 +25,15 @@ impl Argument { ) -> Result, Error> where CS::Commitment: Hashable, + ::Input: + crate::transcript::TranscriptInputBytes, { let trash_commitment = transcript.read()?; + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_proof_commitment::( + "proof_trash_commitment", + &trash_commitment, + ); Ok(Committed { trash_commitment }) } } @@ -38,8 +45,15 @@ impl> Committed { ) -> Result, Error> where F: Hashable, + ::Input: + crate::transcript::TranscriptInputBytes, { let trash_eval = transcript.read()?; + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_proof_eval::( + "proof_trash_eval", + &trash_eval, + ); Ok(Evaluated { committed: self, diff --git a/proofs/src/plonk/verifier.rs b/proofs/src/plonk/verifier.rs index 87afcb195..b76fabefc 100644 --- a/proofs/src/plonk/verifier.rs +++ b/proofs/src/plonk/verifier.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "solidity-verifier-trace")] +use std::collections::BTreeMap; use std::{ hash::Hash, iter::{self}, @@ -12,7 +14,7 @@ use crate::{ traces::VerifierTrace, }, poly::{commitment::PolynomialCommitmentScheme, CommitmentLabel, VerifierQuery}, - transcript::{read_n, Hashable, Sampleable, Transcript}, + transcript::{read_n, Hashable, Sampleable, Transcript, TranscriptInputBytes}, utils::arithmetic::compute_inner_product, }; @@ -39,6 +41,7 @@ where + FromUniformBytes<64> + Ord, CS::Commitment: Hashable, + ::Input: TranscriptInputBytes, { #[cfg(not(feature = "committed-instances"))] let committed_instances: Vec> = vec![vec![]; instances.len()]; @@ -65,6 +68,24 @@ where // Hash verification key into transcript vk.hash_into(transcript)?; + #[cfg(feature = "solidity-verifier-trace")] + { + crate::plonk::solidity_trace::record_hashable::( + 1, + "vk_digest", + &vk.transcript_repr(), + ); + let num_instances = instances + .first() + .and_then(|cols| cols.first()) + .map(|values| values.len()) + .unwrap_or_default(); + crate::plonk::solidity_trace::record_hashable::( + 2, + "num_instances", + &F::from_u128(num_instances as u128), + ); + } for committed_instances in committed_instances.iter() { for commitment in committed_instances.iter() { @@ -95,12 +116,29 @@ where { if current_phase == *phase { *commitment = transcript.read()?; + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_proof_commitment::( + "proof_advice_commitment", + &*commitment, + ); } } } - for (phase, challenge) in vk.cs.challenge_phase.iter().zip(challenges.iter_mut()) { + for (challenge_idx, (phase, challenge)) in + vk.cs.challenge_phase.iter().zip(challenges.iter_mut()).enumerate() + { + #[cfg(not(feature = "solidity-verifier-trace"))] + let _ = challenge_idx; + if current_phase == *phase { *challenge = transcript.squeeze_challenge(); + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_hashable::( + crate::plonk::solidity_trace::USER_CHALLENGE_TRACE_BASE + + challenge_idx as u64, + "user_challenge", + &*challenge, + ); } } } @@ -110,6 +148,8 @@ where // Sample theta challenge for keeping lookup columns linearly independent let theta: F = transcript.squeeze_challenge(); + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_hashable::(7, "theta", &theta); // Read multiplicities let lookup_multiplicities = (0..num_proofs) @@ -125,9 +165,13 @@ where // Sample beta challenge let beta: F = transcript.squeeze_challenge(); + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_hashable::(8, "beta", &beta); // Sample gamma challenge let gamma: F = transcript.squeeze_challenge(); + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_hashable::(9, "gamma", &gamma); let permutations_committed = (0..num_proofs) .map(|_| { @@ -148,6 +192,14 @@ where .collect::, _>>()?; let trash_challenge: F = transcript.squeeze_challenge(); + #[cfg(feature = "solidity-verifier-trace")] + if !vk.cs.trashcans.is_empty() { + crate::plonk::solidity_trace::record_hashable::( + 12, + "trash_challenge", + &trash_challenge, + ); + } let trashcans_committed = (0..num_proofs) .map(|_| -> Result, _> { @@ -161,6 +213,8 @@ where // Sample y challenge, which keeps the gates linearly independent. let y: F = transcript.squeeze_challenge(); + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_hashable::(10, "y", &y); Ok(VerifierTrace { advice_commitments, @@ -201,6 +255,7 @@ where + Hash + Ord, CS::Commitment: Hashable, + ::Input: TranscriptInputBytes, { #[cfg(not(feature = "committed-instances"))] let committed_instances: Vec> = vec![vec![]; instances.len()]; @@ -234,10 +289,39 @@ where #[cfg(feature = "single-h-commitment")] let nb_quotient_coms = 1; let quotient_limb_coms = read_n(transcript, nb_quotient_coms)?; + #[cfg(feature = "solidity-verifier-trace")] + { + for commitment in "ient_limb_coms { + crate::plonk::solidity_trace::record_proof_commitment::( + "proof_quotient_commitment", + commitment, + ); + } + } // Sample x challenge, which is used to ensure the circuit is // satisfied with high probability. let x: F = transcript.squeeze_challenge(); + #[cfg(feature = "solidity-verifier-trace")] + { + crate::plonk::solidity_trace::record_u64(3, "k", vk.get_domain().k() as u64); + crate::plonk::solidity_trace::record_hashable::( + 4, + "n_inv", + &F::from(vk.n()).invert().unwrap(), + ); + crate::plonk::solidity_trace::record_hashable::( + 5, + "omega", + &vk.get_domain().get_omega(), + ); + crate::plonk::solidity_trace::record_hashable::( + 6, + "omega_inv", + &vk.get_domain().get_omega_inv(), + ); + crate::plonk::solidity_trace::record_hashable::(11, "x", &x); + } let splitting_factor = x.pow_vartime([vk.n() - 1]); let xn = splitting_factor * x; @@ -271,7 +355,13 @@ where .iter() .map(|(column, rotation)| { if column.index() < nb_committed_instances { - transcript.read() + let eval = transcript.read()?; + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_proof_eval::( + "proof_committed_instance_eval", + &eval, + ); + Ok::(eval) } else { let instances = instances[column.index() - nb_committed_instances]; let offset = (max_rotation - rotation.0) as usize; @@ -287,7 +377,19 @@ where }; let advice_evals = (0..num_proofs) - .map(|_| -> Result, _> { read_n(transcript, vk.cs.advice_queries.len()) }) + .map(|_| -> Result, _> { + let evals = read_n(transcript, vk.cs.advice_queries.len())?; + #[cfg(feature = "solidity-verifier-trace")] + { + for eval in &evals { + crate::plonk::solidity_trace::record_proof_eval::( + "proof_advice_eval", + eval, + ); + } + } + Ok::, Error>(evals) + }) .collect::, _>>()?; // Read (num_fixed_columns - num_simple_selectors) evals and from the transcript @@ -297,6 +399,12 @@ where transcript, vk.cs.num_fixed_columns() - vk.cs.num_simple_selectors(), )?; + #[cfg(feature = "solidity-verifier-trace")] + { + for eval in &fixed_evals { + crate::plonk::solidity_trace::record_proof_eval::("proof_fixed_eval", eval); + } + } for (idx, (col, _)) in vk.cs.fixed_queries().iter().enumerate() { if vk.cs.has_simple_selector_col(col.index()) { fixed_evals.insert(idx, F::ONE) @@ -330,6 +438,40 @@ where }) .collect::, _>>()?; + #[cfg(feature = "solidity-verifier-trace")] + { + let blinding_factors = vk.cs.blinding_factors(); + let l_evals = vk.domain.l_i_range(x, xn, (-((blinding_factors + 1) as i32))..=0); + assert_eq!(l_evals.len(), 2 + blinding_factors); + let l_last = l_evals[0]; + let l_blind = + l_evals[1..(1 + blinding_factors)].iter().fold(F::ZERO, |acc, eval| acc + eval); + let l_0 = l_evals[1 + blinding_factors]; + let x_n_minus_1_inv = (xn - F::ONE).invert().unwrap(); + let instance_eval = vk + .cs + .instance_queries + .iter() + .position(|(column, _)| column.index() >= nb_committed_instances) + .map(|idx| instance_evals[0][idx]) + .unwrap_or(F::ZERO); + + crate::plonk::solidity_trace::record_hashable::(17, "x_n", &xn); + crate::plonk::solidity_trace::record_hashable::( + 18, + "x_n_minus_1_inv", + &x_n_minus_1_inv, + ); + crate::plonk::solidity_trace::record_hashable::(19, "l_last", &l_last); + crate::plonk::solidity_trace::record_hashable::(20, "l_blind", &l_blind); + crate::plonk::solidity_trace::record_hashable::(21, "l_0", &l_0); + crate::plonk::solidity_trace::record_hashable::( + 22, + "instance_eval", + &instance_eval, + ); + } + // Partially evaluate batched identities // (without fixed columns corresponding to simple, multiplicative selectors) let expressions = partially_evaluate_identities( @@ -349,6 +491,46 @@ where trash_challenge, &challenges, ); + #[cfg(feature = "solidity-verifier-trace")] + { + for (idx, (_, eval)) in expressions.iter().enumerate() { + crate::plonk::solidity_trace::record_hashable::( + crate::plonk::solidity_trace::QUOTIENT_IDENTITY_TRACE_BASE + idx as u64, + "quotient_identity_eval", + eval, + ); + } + } + + #[cfg(feature = "solidity-verifier-trace")] + { + let mut selector_folds = BTreeMap::::new(); + let mut quotient_numerator = F::ZERO; + let mut y_pow = F::ONE; + for (col_idx, eval) in expressions.iter().rev() { + match col_idx { + Some(col_idx) => { + *selector_folds.entry(*col_idx).or_insert(F::ZERO) += y_pow * eval; + } + None => { + quotient_numerator += y_pow * eval; + } + } + y_pow *= y; + } + crate::plonk::solidity_trace::record_hashable::( + crate::plonk::solidity_trace::QUOTIENT_NUMERATOR_TRACE_ID, + "quotient_numerator", + "ient_numerator, + ); + for (idx, (_, eval)) in selector_folds.iter().enumerate() { + crate::plonk::solidity_trace::record_hashable::( + crate::plonk::solidity_trace::SELECTOR_FOLD_TRACE_BASE + idx as u64, + "selector_fold", + eval, + ); + } + } let lin_com = compute_linearization_commitment( expressions, @@ -359,6 +541,37 @@ where &splitting_factor, "ient_limb_coms, ); + #[cfg(feature = "solidity-verifier-trace")] + { + crate::plonk::solidity_trace::record_hashable::( + 23, + "linearization_expected_eval", + &lin_com.eval, + ); + + let linearization_commitment = lin_com + .commitment + .as_terms() + .into_iter() + .fold(CS::Commitment::default(), |acc, (scalar, commitment)| { + acc + commitment * scalar + }); + crate::plonk::solidity_trace::record_hashable::( + crate::plonk::solidity_trace::LINEARIZATION_COMMITMENT_TRACE_ID, + "linearization_commitment", + &linearization_commitment, + ); + + let mut linearization_scalars = Vec::with_capacity(0x80); + linearization_scalars.extend_from_slice(&splitting_factor.to_input().into_trace_bytes()); + linearization_scalars.extend_from_slice(&(F::ONE - xn).to_input().into_trace_bytes()); + linearization_scalars.extend_from_slice(&[0u8; 0x40]); + crate::plonk::solidity_trace::record_bytes( + 24, + "linearization_scalars", + linearization_scalars, + ); + } // Collect queries that are checked in the multi-open argument // @@ -462,6 +675,7 @@ where + Hash + Ord, CS::Commitment: Hashable, + ::Input: TranscriptInputBytes, { let trace = parse_trace( vk, diff --git a/proofs/src/poly/commitment.rs b/proofs/src/poly/commitment.rs index 74c9e3e67..565fac889 100644 --- a/proofs/src/poly/commitment.rs +++ b/proofs/src/poly/commitment.rs @@ -68,7 +68,9 @@ pub trait PolynomialCommitmentScheme: Clone + Debug { ) -> Result where F: Sampleable + Hash + Ord + Hashable, - Self::Commitment: 'com + Hashable; + Self::Commitment: 'com + Hashable, + ::Input: + crate::transcript::TranscriptInputBytes; } /// Interface for verifier finalizer diff --git a/proofs/src/poly/kzg/mod.rs b/proofs/src/poly/kzg/mod.rs index a43d76fea..18d0bd36f 100644 --- a/proofs/src/poly/kzg/mod.rs +++ b/proofs/src/poly/kzg/mod.rs @@ -104,6 +104,8 @@ pub fn scoped_fewer_point_sets(enabled: bool) -> ScopedFewerPointSets { } } +#[cfg(feature = "solidity-verifier-trace")] +use crate::transcript::TranscriptInputBytes; #[cfg(feature = "truncated-challenges")] use crate::utils::arithmetic::{truncate, truncated_powers}; use crate::{ @@ -322,6 +324,8 @@ where where E::Fr: Sampleable + Ord + Hash + Hashable, E::G1: 'com + Hashable + CurveExt, + ::Input: + crate::transcript::TranscriptInputBytes, { #[cfg(feature = "fewer-point-sets")] let queries_with_dummies; @@ -333,11 +337,17 @@ where let pairs: Vec<_> = queries.iter().map(|q| (q.commitment.clone(), q.point)).collect(); for (idx, point) in compute_dummy_queries(&pairs) { + let eval = transcript.read().map_err(|_| Error::SamplingError)?; + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_proof_eval::( + "proof_dummy_eval", + &eval, + ); queries.push(VerifierQuery { point, commitment_label: queries[idx].commitment_label.clone(), commitment: queries[idx].commitment.clone(), - eval: transcript.read().map_err(|_| Error::SamplingError)?, + eval, }); } queries @@ -351,6 +361,11 @@ where // https://zcash.github.io/halo2/design/proving-system/multipoint-opening.html let x1: E::Fr = transcript.squeeze_challenge(); let x2: E::Fr = transcript.squeeze_challenge(); + #[cfg(feature = "solidity-verifier-trace")] + { + crate::plonk::solidity_trace::record_hashable::(13, "x1", &x1); + crate::plonk::solidity_trace::record_hashable::(14, "x2", &x2); + } let (commitment_map, point_sets) = construct_intermediate_sets(queries)?; @@ -406,18 +421,48 @@ where let point_sets: Vec<_> = order.iter().map(|&i| point_sets[i].clone()).collect(); (q_coms, q_eval_sets, point_sets) }; + #[cfg(feature = "solidity-verifier-trace")] + { + for (idx, points) in point_sets.iter().enumerate() { + let mut data = Vec::with_capacity(points.len() * 32); + for point in points { + data.extend_from_slice(&point.to_input().into_trace_bytes()); + } + crate::plonk::solidity_trace::record_bytes( + crate::plonk::solidity_trace::PCS_POINT_SET_TRACE_BASE + idx as u64, + "pcs_point_set", + data, + ); + } + for (idx, q_com) in q_coms.iter().enumerate() { + crate::plonk::solidity_trace::record_hashable::( + crate::plonk::solidity_trace::PCS_Q_COM_TRACE_BASE + idx as u64, + "pcs_q_com", + &q_com.eval(), + ); + } + } let f_com: E::G1 = transcript.read().map_err(|_| Error::SamplingError)?; + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_proof_commitment::("proof_f_com", &f_com); + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_hashable::(25, "f_com", &f_com); // Sample a challenge x_3 for checking that f(X) was committed to // correctly. let x3: E::Fr = transcript.squeeze_challenge(); #[cfg(feature = "truncated-challenges")] let x3 = truncate(x3); + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_hashable::(15, "x3", &x3); let mut q_evals_on_x3 = Vec::::with_capacity(q_eval_sets.len()); for _ in 0..q_eval_sets.len() { - q_evals_on_x3.push(transcript.read().map_err(|_| Error::SamplingError)?); + let eval = transcript.read().map_err(|_| Error::SamplingError)?; + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_proof_eval::("proof_q_eval", &eval); + q_evals_on_x3.push(eval); } // We can compute the expected msm_eval at x_3 using the u provided @@ -436,6 +481,8 @@ where ); let x4: E::Fr = transcript.squeeze_challenge(); + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_hashable::(16, "x4", &x4); let final_com = { let size = q_coms.len() + 1; @@ -458,6 +505,12 @@ where msm_inner_product(coms, &powers.take(size).collect::>()) }; + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_hashable::( + 33, + "final_com", + &final_com.eval(), + ); let v = { let mut evals = q_evals_on_x3; @@ -471,8 +524,17 @@ where inner_product(&evals, powers) }; + #[cfg(feature = "solidity-verifier-trace")] + { + crate::plonk::solidity_trace::record_hashable::(31, "f_eval", &f_eval); + crate::plonk::solidity_trace::record_hashable::(32, "v", &v); + } let pi: E::G1 = transcript.read().map_err(|_| Error::SamplingError)?; + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_proof_commitment::("proof_pi", &pi); + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_hashable::(26, "pi", &pi); let mut pi_msm = MSMKZG::::init(); pi_msm.append_term(E::Fr::ONE, pi, CommitmentLabel::Custom("Ï€".into())); @@ -493,6 +555,19 @@ where right: final_com, }; msm_accumulator.right.add_msm(&scaled_pi); + #[cfg(feature = "solidity-verifier-trace")] + { + crate::plonk::solidity_trace::record_hashable::( + 27, + "pairing_lhs", + &msm_accumulator.left.eval(), + ); + crate::plonk::solidity_trace::record_hashable::( + 28, + "pairing_rhs", + &msm_accumulator.right.eval(), + ); + } Ok(msm_accumulator) } diff --git a/proofs/src/poly/kzg/msm.rs b/proofs/src/poly/kzg/msm.rs index fd4db6f03..f4e011184 100644 --- a/proofs/src/poly/kzg/msm.rs +++ b/proofs/src/poly/kzg/msm.rs @@ -296,6 +296,14 @@ where ); let terms = &[term_1, term_2]; - bool::from(E::multi_miller_loop(&terms[..]).final_exponentiation().is_identity()) + let result = + bool::from(E::multi_miller_loop(&terms[..]).final_exponentiation().is_identity()); + #[cfg(feature = "solidity-verifier-trace")] + crate::plonk::solidity_trace::record_u64( + crate::plonk::solidity_trace::FINAL_RESULT_TRACE_ID, + "final_result", + result as u64, + ); + result } } diff --git a/proofs/src/transcript/mod.rs b/proofs/src/transcript/mod.rs index 15f498f9c..1bcbecaed 100644 --- a/proofs/src/transcript/mod.rs +++ b/proofs/src/transcript/mod.rs @@ -4,12 +4,39 @@ mod implementors; use std::io::{self, Cursor, Read, Write}; +use ff::PrimeField; + /// Prefix to a prover's message soliciting a challenge const BLAKE2B_PREFIX_CHALLENGE: u8 = 0; /// Prefix to a prover's message const BLAKE2B_PREFIX_COMMON: u8 = 1; +/// A transcript hash input that can be rendered as bytes for diagnostics. +/// +/// The byte representation is used only by verifier trace hooks; the +/// transcript itself continues to absorb the native `Input` type. +pub trait TranscriptInputBytes { + /// Convert the input into a deterministic diagnostic byte string. + fn into_trace_bytes(self) -> Vec; +} + +impl TranscriptInputBytes for Vec { + fn into_trace_bytes(self) -> Vec { + self + } +} + +impl TranscriptInputBytes for Vec { + fn into_trace_bytes(self) -> Vec { + let mut bytes = Vec::new(); + for value in self { + bytes.extend_from_slice(value.to_repr().as_ref()); + } + bytes + } +} + /// Hash function that can be used for transcript pub trait TranscriptHash: Clone { /// Input type of the hash function diff --git a/zk_stdlib/src/lib.rs b/zk_stdlib/src/lib.rs index 6f37af460..982cf41af 100644 --- a/zk_stdlib/src/lib.rs +++ b/zk_stdlib/src/lib.rs @@ -93,7 +93,9 @@ use midnight_proofs::{ KZGCommitmentScheme, }, }, - transcript::{CircuitTranscript, Hashable, Sampleable, Transcript, TranscriptHash}, + transcript::{ + CircuitTranscript, Hashable, Sampleable, Transcript, TranscriptHash, TranscriptInputBytes, + }, utils::SerdeFormat, }; use num_bigint::BigUint; @@ -1775,6 +1777,7 @@ pub fn prove( where G1Projective: Hashable, F: Hashable + Sampleable, + H::Input: TranscriptInputBytes, { let pi = R::format_instance(instance)?; let com_inst = R::format_committed_instances(&witness); @@ -1808,6 +1811,7 @@ pub fn verify( where G1Projective: Hashable, F: Hashable + Sampleable, + H::Input: TranscriptInputBytes, { let pi = R::format_instance(instance)?; let committed_pi = committed_instance.unwrap_or(G1Affine::identity()); @@ -1839,6 +1843,7 @@ pub fn batch_verify( where G1Projective: Hashable, F: Hashable + Sampleable, + H::Input: TranscriptInputBytes, { use rayon::prelude::*; diff --git a/zk_stdlib/src/utils/plonk_api.rs b/zk_stdlib/src/utils/plonk_api.rs index a5a833340..0e6b7bea6 100644 --- a/zk_stdlib/src/utils/plonk_api.rs +++ b/zk_stdlib/src/utils/plonk_api.rs @@ -35,7 +35,9 @@ use midnight_proofs::{ KZGCommitmentScheme, }, }, - transcript::{CircuitTranscript, Hashable, Sampleable, Transcript, TranscriptHash}, + transcript::{ + CircuitTranscript, Hashable, Sampleable, Transcript, TranscriptHash, TranscriptInputBytes, + }, utils::SerdeFormat, }; use rand::{CryptoRng, RngCore}; @@ -95,6 +97,7 @@ macro_rules! plonk_api { ) -> Result, Error> where H: TranscriptHash, + H::Input: TranscriptInputBytes, $projective: Hashable, $native: Hashable + Sampleable, { @@ -138,6 +141,7 @@ macro_rules! plonk_api { ) -> Result<(), Error> where H: TranscriptHash, + H::Input: TranscriptInputBytes, $projective: Hashable, $native: Hashable + Sampleable, { From cf89e5f3bcd108a2566724004f5bd237a77f4613 Mon Sep 17 00:00:00 2001 From: Julien Coolen Date: Fri, 15 May 2026 19:18:25 +0200 Subject: [PATCH 05/19] feat(solidity): add Midfall verifier workspace Move the Solidity verifier generator into the Midfall workspace with codegen, templates, docs, fixtures, scripts, tests, and validated test-command documentation. --- Cargo.toml | 10 +- aggregation/CHANGELOG.md | 1 + aggregation/src/ivc/prover.rs | 5 +- aggregation/tests/ivc_keccak_final.rs | 15 +- circuits/CHANGELOG.md | 1 + proofs/CHANGELOG.md | 2 + proofs/benches/plonk.rs | 5 +- .../.github/workflows/ci.yml | 153 + proofs/solidity-verifier/.gitignore | 20 + proofs/solidity-verifier/Cargo.toml | 94 + proofs/solidity-verifier/LICENSE | 21 + proofs/solidity-verifier/README.md | 399 + proofs/solidity-verifier/askama.toml | 3 + .../docs/architecture/ARCHITECTURE.md | 194 + .../LOWERING_ARCHITECTURE_SPEC.md | 610 + .../docs/architecture/MEMORY_LAYOUT.md | 272 + .../docs/architecture/MIGRATION.md | 518 + proofs/solidity-verifier/docs/audit/AUDIT.md | 2600 ++++ .../docs/audit/AUDIT_FINDINGS.md | 1419 ++ .../docs/audit/CODEGEN_ASSURANCE_DOSSIER.md | 164 + .../docs/audit/REVIEW_PACKET.md | 252 + .../docs/benchmarks/BENCH.md | 626 + .../FEWER_POINT_SETS_MSM_ANALYSIS.md | 88 + .../docs/benchmarks/FEWER_POINT_SETS_NOTES.md | 233 + .../benchmarks/IVC_LATEST_BENCH_RESULTS.md | 143 + .../docs/benchmarks/OPTIMISATION.md | 273 + .../PCS_BLOCK5_FINAL_MSM_ANALYSIS.md | 187 + .../benchmarks/PCS_FUSED_MSM_OPTIMISATIONS.md | 161 + .../docs/plans/FURTHER_OPTIMISATIONS.md | 186 + .../docs/plans/IMPROVEMENTS.md | 71 + .../plans/IVC_GAS_OPTIMISATION_ACTION_PLAN.md | 431 + .../solidity-verifier/docs/plans/ROADMAP.md | 555 + .../docs/plans/SPLIT_NWAY_NOTES.md | 143 + .../docs/plans/TESTING_STRATEGY.md | 2589 ++++ .../docs/plans/quotient_eval_optim.md | 475 + .../plans/quotient_size_reduction_plan.md | 367 + .../docs/plans/spec-migration.md | 119 + .../reference/ASKAMA_TEMPLATE_RUST_MAPPING.md | 438 + .../reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md | 1711 +++ .../MIDFALL_PROOFS_COMMENT_CORPUS.md | 10966 ++++++++++++++++ .../docs/reference/PORTING_NOTES.md | 195 + .../docs/reference/PR17_BORROWED_IDEAS.md | 72 + .../QUOTIENT_EVALUATOR_9KB_BYTECODE.md | 294 + .../reference/QUOTIENT_NUMERATOR_EVALUATOR.md | 1068 ++ .../docs/reference/REPRODUCIBLE_BUILDS.md | 119 + .../docs/reference/STATUS.md | 257 + .../docs/reference/TEAM_DEMO_SETUP.md | 478 + .../docs/reference/TESTING.md | 504 + .../docs/reference/TRACE_VARIABLES.md | 92 + .../solidity-verifier/examples/ivc_replay.rs | 137 + proofs/solidity-verifier/rust-toolchain.toml | 2 + .../scripts/check_release_bytecode_sizes.sh | 52 + .../scripts/ensure_srs_assets.sh | 49 + .../scripts/extract_midfall_comments.py | 226 + .../scripts/install_pinned_solc.sh | 37 + .../scripts/run_ivc_bench.sh | 366 + .../scripts/run_team_demo.sh | 587 + proofs/solidity-verifier/src/api.rs | 588 + proofs/solidity-verifier/src/builder/api.rs | 124 + proofs/solidity-verifier/src/builder/mod.rs | 68 + .../solidity-verifier/src/builder/render.rs | 128 + .../solidity-verifier/src/builder/repack.rs | 27 + proofs/solidity-verifier/src/builder/tests.rs | 130 + proofs/solidity-verifier/src/evm.rs | 500 + proofs/solidity-verifier/src/lib.rs | 76 + .../solidity-verifier/src/lowering/abi/mod.rs | 9 + .../src/lowering/abi/proof.rs | 476 + .../src/lowering/artifacts.rs | 299 + .../src/lowering/calldata.rs | 144 + .../solidity-verifier/src/lowering/config.rs | 27 + .../src/lowering/diagnostics.rs | 215 + .../src/lowering/encoding/mod.rs | 921 ++ .../solidity-verifier/src/lowering/kzg/mod.rs | 2084 +++ .../src/lowering/layout/memory.rs | 1427 ++ .../src/lowering/layout/mod.rs | 771 ++ .../src/lowering/layout/vk_payload.rs | 322 + proofs/solidity-verifier/src/lowering/mod.rs | 49 + proofs/solidity-verifier/src/lowering/plan.rs | 309 + .../src/lowering/protocol/mod.rs | 1223 ++ .../src/lowering/quotient.rs | 1927 +++ .../src/lowering/quotient_numerator/mod.rs | 11 + .../src/lowering/quotient_numerator/vm/mod.rs | 3793 ++++++ .../lowering/quotient_numerator/yul_emit.rs | 1161 ++ .../src/lowering/render/mod.rs | 11 + .../src/lowering/render/models.rs | 1369 ++ .../src/lowering/render/yul.rs | 5 + .../solidity-verifier/src/lowering/tests.rs | 3261 +++++ proofs/solidity-verifier/src/lowering/vk.rs | 443 + proofs/solidity-verifier/src/test.rs | 2836 ++++ proofs/solidity-verifier/src/trace_replay.rs | 296 - .../contracts/Halo2QuotientEvaluator.sol | 167 + .../templates/contracts/Halo2Verifier.sol | 128 + .../templates/contracts/Halo2VerifyingKey.sol | 92 + .../quotient_numerator/QuotientHelpers.yul | 71 + .../QuotientNumeratorBlock.yul | 1269 ++ .../partials/verifier/AccumulatorHelpers.yul | 396 + .../partials/verifier/AssemblyHelpers.yul | 232 + .../templates/partials/verifier/Constants.sol | 177 + .../partials/verifier/Constructors.sol | 90 + .../partials/verifier/FinalPairing.yul | 83 + .../templates/partials/verifier/Lagrange.yul | 89 + .../templates/partials/verifier/Pcs.yul | 39 + .../partials/verifier/PrecompileSmoke.sol | 37 + .../verifier/QuotientAndLinearization.yul | 133 + .../partials/verifier/TraceAndGasHelpers.yul | 23 + .../partials/verifier/TraceReturn.yul | 58 + .../verifier/TranscriptProofParser.yul | 423 + .../templates/partials/verifier/VkLoading.yul | 126 + .../tests/fcom_decompress.rs | 57 + .../tests/ivc_keccak_solidity.rs | 1794 +++ .../tests/poseidon_fixture.rs | 563 + proofs/src/plonk/prover.rs | 3 +- proofs/src/poly/kzg/mod.rs | 14 +- proofs/src/transcript/implementors.rs | 5 +- proofs/tests/plonk_api.rs | 15 +- zk_stdlib/CHANGELOG.md | 2 + zk_stdlib/examples/poseidon.rs | 17 +- 117 files changed, 61861 insertions(+), 327 deletions(-) create mode 100644 proofs/solidity-verifier/.github/workflows/ci.yml create mode 100644 proofs/solidity-verifier/Cargo.toml create mode 100644 proofs/solidity-verifier/LICENSE create mode 100644 proofs/solidity-verifier/README.md create mode 100644 proofs/solidity-verifier/askama.toml create mode 100644 proofs/solidity-verifier/docs/architecture/ARCHITECTURE.md create mode 100644 proofs/solidity-verifier/docs/architecture/LOWERING_ARCHITECTURE_SPEC.md create mode 100644 proofs/solidity-verifier/docs/architecture/MEMORY_LAYOUT.md create mode 100644 proofs/solidity-verifier/docs/architecture/MIGRATION.md create mode 100644 proofs/solidity-verifier/docs/audit/AUDIT.md create mode 100644 proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md create mode 100644 proofs/solidity-verifier/docs/audit/CODEGEN_ASSURANCE_DOSSIER.md create mode 100644 proofs/solidity-verifier/docs/audit/REVIEW_PACKET.md create mode 100644 proofs/solidity-verifier/docs/benchmarks/BENCH.md create mode 100644 proofs/solidity-verifier/docs/benchmarks/FEWER_POINT_SETS_MSM_ANALYSIS.md create mode 100644 proofs/solidity-verifier/docs/benchmarks/FEWER_POINT_SETS_NOTES.md create mode 100644 proofs/solidity-verifier/docs/benchmarks/IVC_LATEST_BENCH_RESULTS.md create mode 100644 proofs/solidity-verifier/docs/benchmarks/OPTIMISATION.md create mode 100644 proofs/solidity-verifier/docs/benchmarks/PCS_BLOCK5_FINAL_MSM_ANALYSIS.md create mode 100644 proofs/solidity-verifier/docs/benchmarks/PCS_FUSED_MSM_OPTIMISATIONS.md create mode 100644 proofs/solidity-verifier/docs/plans/FURTHER_OPTIMISATIONS.md create mode 100644 proofs/solidity-verifier/docs/plans/IMPROVEMENTS.md create mode 100644 proofs/solidity-verifier/docs/plans/IVC_GAS_OPTIMISATION_ACTION_PLAN.md create mode 100644 proofs/solidity-verifier/docs/plans/ROADMAP.md create mode 100644 proofs/solidity-verifier/docs/plans/SPLIT_NWAY_NOTES.md create mode 100644 proofs/solidity-verifier/docs/plans/TESTING_STRATEGY.md create mode 100644 proofs/solidity-verifier/docs/plans/quotient_eval_optim.md create mode 100644 proofs/solidity-verifier/docs/plans/quotient_size_reduction_plan.md create mode 100644 proofs/solidity-verifier/docs/plans/spec-migration.md create mode 100644 proofs/solidity-verifier/docs/reference/ASKAMA_TEMPLATE_RUST_MAPPING.md create mode 100644 proofs/solidity-verifier/docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md create mode 100644 proofs/solidity-verifier/docs/reference/MIDFALL_PROOFS_COMMENT_CORPUS.md create mode 100644 proofs/solidity-verifier/docs/reference/PORTING_NOTES.md create mode 100644 proofs/solidity-verifier/docs/reference/PR17_BORROWED_IDEAS.md create mode 100644 proofs/solidity-verifier/docs/reference/QUOTIENT_EVALUATOR_9KB_BYTECODE.md create mode 100644 proofs/solidity-verifier/docs/reference/QUOTIENT_NUMERATOR_EVALUATOR.md create mode 100644 proofs/solidity-verifier/docs/reference/REPRODUCIBLE_BUILDS.md create mode 100644 proofs/solidity-verifier/docs/reference/STATUS.md create mode 100644 proofs/solidity-verifier/docs/reference/TEAM_DEMO_SETUP.md create mode 100644 proofs/solidity-verifier/docs/reference/TESTING.md create mode 100644 proofs/solidity-verifier/docs/reference/TRACE_VARIABLES.md create mode 100644 proofs/solidity-verifier/examples/ivc_replay.rs create mode 100644 proofs/solidity-verifier/rust-toolchain.toml create mode 100755 proofs/solidity-verifier/scripts/check_release_bytecode_sizes.sh create mode 100755 proofs/solidity-verifier/scripts/ensure_srs_assets.sh create mode 100755 proofs/solidity-verifier/scripts/extract_midfall_comments.py create mode 100755 proofs/solidity-verifier/scripts/install_pinned_solc.sh create mode 100755 proofs/solidity-verifier/scripts/run_ivc_bench.sh create mode 100755 proofs/solidity-verifier/scripts/run_team_demo.sh create mode 100644 proofs/solidity-verifier/src/api.rs create mode 100644 proofs/solidity-verifier/src/builder/api.rs create mode 100644 proofs/solidity-verifier/src/builder/mod.rs create mode 100644 proofs/solidity-verifier/src/builder/render.rs create mode 100644 proofs/solidity-verifier/src/builder/repack.rs create mode 100644 proofs/solidity-verifier/src/builder/tests.rs create mode 100644 proofs/solidity-verifier/src/evm.rs create mode 100644 proofs/solidity-verifier/src/lib.rs create mode 100644 proofs/solidity-verifier/src/lowering/abi/mod.rs create mode 100644 proofs/solidity-verifier/src/lowering/abi/proof.rs create mode 100644 proofs/solidity-verifier/src/lowering/artifacts.rs create mode 100644 proofs/solidity-verifier/src/lowering/calldata.rs create mode 100644 proofs/solidity-verifier/src/lowering/config.rs create mode 100644 proofs/solidity-verifier/src/lowering/diagnostics.rs create mode 100644 proofs/solidity-verifier/src/lowering/encoding/mod.rs create mode 100644 proofs/solidity-verifier/src/lowering/kzg/mod.rs create mode 100644 proofs/solidity-verifier/src/lowering/layout/memory.rs create mode 100644 proofs/solidity-verifier/src/lowering/layout/mod.rs create mode 100644 proofs/solidity-verifier/src/lowering/layout/vk_payload.rs create mode 100644 proofs/solidity-verifier/src/lowering/mod.rs create mode 100644 proofs/solidity-verifier/src/lowering/plan.rs create mode 100644 proofs/solidity-verifier/src/lowering/protocol/mod.rs create mode 100644 proofs/solidity-verifier/src/lowering/quotient.rs create mode 100644 proofs/solidity-verifier/src/lowering/quotient_numerator/mod.rs create mode 100644 proofs/solidity-verifier/src/lowering/quotient_numerator/vm/mod.rs create mode 100644 proofs/solidity-verifier/src/lowering/quotient_numerator/yul_emit.rs create mode 100644 proofs/solidity-verifier/src/lowering/render/mod.rs create mode 100644 proofs/solidity-verifier/src/lowering/render/models.rs create mode 100644 proofs/solidity-verifier/src/lowering/render/yul.rs create mode 100644 proofs/solidity-verifier/src/lowering/tests.rs create mode 100644 proofs/solidity-verifier/src/lowering/vk.rs create mode 100644 proofs/solidity-verifier/src/test.rs delete mode 100644 proofs/solidity-verifier/src/trace_replay.rs create mode 100644 proofs/solidity-verifier/templates/contracts/Halo2QuotientEvaluator.sol create mode 100644 proofs/solidity-verifier/templates/contracts/Halo2Verifier.sol create mode 100644 proofs/solidity-verifier/templates/contracts/Halo2VerifyingKey.sol create mode 100644 proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientHelpers.yul create mode 100644 proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientNumeratorBlock.yul create mode 100644 proofs/solidity-verifier/templates/partials/verifier/AccumulatorHelpers.yul create mode 100644 proofs/solidity-verifier/templates/partials/verifier/AssemblyHelpers.yul create mode 100644 proofs/solidity-verifier/templates/partials/verifier/Constants.sol create mode 100644 proofs/solidity-verifier/templates/partials/verifier/Constructors.sol create mode 100644 proofs/solidity-verifier/templates/partials/verifier/FinalPairing.yul create mode 100644 proofs/solidity-verifier/templates/partials/verifier/Lagrange.yul create mode 100644 proofs/solidity-verifier/templates/partials/verifier/Pcs.yul create mode 100644 proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol create mode 100644 proofs/solidity-verifier/templates/partials/verifier/QuotientAndLinearization.yul create mode 100644 proofs/solidity-verifier/templates/partials/verifier/TraceAndGasHelpers.yul create mode 100644 proofs/solidity-verifier/templates/partials/verifier/TraceReturn.yul create mode 100644 proofs/solidity-verifier/templates/partials/verifier/TranscriptProofParser.yul create mode 100644 proofs/solidity-verifier/templates/partials/verifier/VkLoading.yul create mode 100644 proofs/solidity-verifier/tests/fcom_decompress.rs create mode 100644 proofs/solidity-verifier/tests/ivc_keccak_solidity.rs create mode 100644 proofs/solidity-verifier/tests/poseidon_fixture.rs diff --git a/Cargo.toml b/Cargo.toml index 5d891c0eb..f32794706 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,15 @@ [workspace] resolver = "2" -members = ["proofs", "curves", "circuits", "aggregation", "zkir", "zk_stdlib"] +members = [ + "proofs", + "proofs/solidity-verifier", + "curves", + "circuits", + "aggregation", + "zkir", + "zk_stdlib", +] package = { license-file = "LICENSE" } diff --git a/aggregation/CHANGELOG.md b/aggregation/CHANGELOG.md index b11743c0d..0c6bd7622 100644 --- a/aggregation/CHANGELOG.md +++ b/aggregation/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://book.async.rs/overview ## [Unreleased] ### Added +* Add Keccak final IVC aggregation tests used by the Solidity verifier bench. * `fewer-point-sets` feature [#281](https://github.com/midnightntwrk/midnight-zk/pull/281) * `single-h-commitment` feature [#276](https://github.com/midnightntwrk/midnight-zk/pull/276) * Introduce `IvcIO` trait and `Ivc` convenience trait [#264](https://github.com/midnightntwrk/midnight-zk/pull/264) diff --git a/aggregation/src/ivc/prover.rs b/aggregation/src/ivc/prover.rs index 9dfe3eeb4..7b7a79e3f 100644 --- a/aggregation/src/ivc/prover.rs +++ b/aggregation/src/ivc/prover.rs @@ -170,9 +170,8 @@ impl IvcProver { /// `PoseidonState` (since prior steps emitted Poseidon-transcript /// proofs). /// - The IVC circuit's *in-circuit* re-verification of the previous proof - /// also still uses Poseidon (the IVC gadget hard-codes - /// `PoseidonState`); this is what makes prior-proof verification cheap - /// in-circuit. + /// also still uses Poseidon (the IVC gadget hard-codes `PoseidonState`); + /// this is what makes prior-proof verification cheap in-circuit. /// - Only the **outer** Fiat-Shamir transcript used to produce the new /// final proof switches to Keccak. /// diff --git a/aggregation/tests/ivc_keccak_final.rs b/aggregation/tests/ivc_keccak_final.rs index dffe2a0cf..6d9b5d777 100644 --- a/aggregation/tests/ivc_keccak_final.rs +++ b/aggregation/tests/ivc_keccak_final.rs @@ -170,16 +170,18 @@ fn ivc_keccak_final_round_trip() { use std::time::Instant; const K: u32 = 18; - let srs = load_srs(SrsSource::Filecoin, K, IvcCircuit::::cs_degree()); + let srs = load_srs( + SrsSource::Filecoin, + K, + IvcCircuit::::cs_degree(), + ); let setup_t = Instant::now(); let (mut prover, verifier) = ivc::setup::(srs, K, ()); println!("[ivc-keccak] setup completed in {:.2?}", setup_t.elapsed()); let prove_t = Instant::now(); - let proof = prover - .prove_final_step(()) - .expect("prove_final_step should succeed at genesis"); + let proof = prover.prove_final_step(()).expect("prove_final_step should succeed at genesis"); println!( "[ivc-keccak] prove_final_step completed in {:.2?} ({} bytes)", prove_t.elapsed(), @@ -193,7 +195,10 @@ fn ivc_keccak_final_round_trip() { verifier .verify_final::(&(), &instance, &proof) .expect("verify_final should accept a prove_final_step output"); - println!("[ivc-keccak] verify_final completed in {:.2?}", verify_t.elapsed()); + println!( + "[ivc-keccak] verify_final completed in {:.2?}", + verify_t.elapsed() + ); // Cross-check: regular verify (Poseidon transcript) must REJECT a // Keccak-transcript proof. This guards against accidental fall-back diff --git a/circuits/CHANGELOG.md b/circuits/CHANGELOG.md index dad24d834..f0c703b54 100644 --- a/circuits/CHANGELOG.md +++ b/circuits/CHANGELOG.md @@ -29,6 +29,7 @@ verification keys break backwards compatibility. * Fix cost model to pass correct number of committed instances [#280](https://github.com/midnightntwrk/midnight-zk/pull/280) ### Changed +* Expose verifier accumulator helpers needed by the Solidity verifier bench. * Split linearization commitment into non-constant and constant parts, removing the generator point from the MSM [#313](https://github.com/midnightntwrk/midnight-zk/pull/313) * Make `NB_ARITH_COLS` configurable instead of a compile-time constant [#287](https://github.com/midnightntwrk/midnight-zk/pull/287) * Support `fewer-point-sets` feature in verifier gadget [#281](https://github.com/midnightntwrk/midnight-zk/pull/281) diff --git a/proofs/CHANGELOG.md b/proofs/CHANGELOG.md index 487aca631..0037caefa 100644 --- a/proofs/CHANGELOG.md +++ b/proofs/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +* Add the `halo2_solidity_verifier` crate with Solidity verifier rendering, + EVM test harnesses, IVC/Moonlight bench tooling, and audit documentation. * changed `sha256` name in benches to account for the change of naming convention in `circuits` [#135](https://github.com/midnightntwrk/midnight-zk/pull/135) * optional names on VerifierQuery commitments [#205](https://github.com/midnightntwrk/midnight-zk/pull/205) * `padded_add` and `padded_sub` polynomial operations [#276](https://github.com/midnightntwrk/midnight-zk/pull/276) diff --git a/proofs/benches/plonk.rs b/proofs/benches/plonk.rs index 9810ccf6d..655ba2aeb 100644 --- a/proofs/benches/plonk.rs +++ b/proofs/benches/plonk.rs @@ -3,6 +3,7 @@ extern crate criterion; use std::marker::PhantomData; +use blake2b_simd::State; use criterion::{BenchmarkId, Criterion}; use group::ff::Field; use midnight_curves::{Bls12, Fq as Scalar}; @@ -280,7 +281,7 @@ fn criterion_benchmark(c: &mut Criterion) { k, }; - let mut transcript = CircuitTranscript::init(); + let mut transcript = CircuitTranscript::::init(); create_proof::, _, _>( params, @@ -301,7 +302,7 @@ fn criterion_benchmark(c: &mut Criterion) { vk: &VerifyingKey>, proof: &[u8], ) { - let mut transcript = CircuitTranscript::init_from_bytes(proof); + let mut transcript = CircuitTranscript::::init_from_bytes(proof); assert!(prepare::, _>( vk, #[cfg(feature = "committed-instances")] diff --git a/proofs/solidity-verifier/.github/workflows/ci.yml b/proofs/solidity-verifier/.github/workflows/ci.yml new file mode 100644 index 000000000..a41ec7a69 --- /dev/null +++ b/proofs/solidity-verifier/.github/workflows/ci.yml @@ -0,0 +1,153 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + - "codex/**" + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + SOLC_INSTALL_DIR: ${{ github.workspace }}/.solc + SRS_DIR: ${{ github.workspace }}/.srs + +jobs: + default-rust-tests: + name: default Rust tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: 1.90.0 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + - name: Install pinned solc + run: | + echo "SOLC=$(scripts/install_pinned_solc.sh "$SOLC_INSTALL_DIR" | tail -1)" >> "$GITHUB_ENV" + - run: cargo test --workspace --all-features --all-targets -- --nocapture + + real-evm-pbt-and-poseidon: + name: real EVM, PBT, Poseidon fixture + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: 1.90.0 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + - name: Cache SRS assets + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 + with: + path: .srs + key: srs-poseidon-ivc-v1 + - name: Install pinned solc + run: | + echo "SOLC=$(scripts/install_pinned_solc.sh "$SOLC_INSTALL_DIR" | tail -1)" >> "$GITHUB_ENV" + - name: Ensure SRS assets + run: scripts/ensure_srs_assets.sh + - name: Real EVM PBT + run: | + HALO2_SOLIDITY_RUN_EVM_TESTS=1 \ + cargo test --release --all-features pbt_ -- --nocapture + - name: Poseidon fixture + run: | + HALO2_SOLIDITY_RUN_EVM_TESTS=1 \ + cargo test --release --features evm,truncated-challenges --test poseidon_fixture -- --nocapture + + poseidon-trace-equivalence: + name: Poseidon native/Solidity trace equivalence + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: 1.90.0 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + - name: Cache SRS assets + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 + with: + path: .srs + key: srs-poseidon-ivc-v1 + - name: Install pinned solc + run: | + echo "SOLC=$(scripts/install_pinned_solc.sh "$SOLC_INSTALL_DIR" | tail -1)" >> "$GITHUB_ENV" + - name: Ensure SRS assets + run: scripts/ensure_srs_assets.sh + - name: Poseidon trace equivalence + run: | + HALO2_SOLIDITY_RUN_EVM_TESTS=1 \ + cargo test --release \ + --features evm,truncated-challenges,rust-verifier-trace,solidity-trace \ + --lib native_midfall_verifier_trace_matches_solidity_trace \ + -- --nocapture + + full-ivc-bench: + name: full IVC bench and release bytecode sizes + runs-on: ubuntu-latest + timeout-minutes: 90 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: 1.90.0 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + - name: Cache SRS assets + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 + with: + path: .srs + key: srs-poseidon-ivc-v1 + - name: Install pinned solc + run: | + echo "SOLC=$(scripts/install_pinned_solc.sh "$SOLC_INSTALL_DIR" | tail -1)" >> "$GITHUB_ENV" + - name: Ensure SRS assets + run: scripts/ensure_srs_assets.sh + - name: Full IVC bench + run: scripts/run_ivc_bench.sh --skip-srs-download + - name: Release bytecode size/hash checks + run: scripts/check_release_bytecode_sizes.sh + - name: Upload IVC artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + if: always() + with: + name: ivc-keccak-solidity-dump + path: target/ivc-keccak-solidity-dump + + ivc-trace-equivalence: + name: IVC native/Solidity trace equivalence + runs-on: ubuntu-latest + timeout-minutes: 90 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: 1.90.0 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + - name: Cache SRS assets + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 + with: + path: .srs + key: srs-poseidon-ivc-v1 + - name: Install pinned solc + run: | + echo "SOLC=$(scripts/install_pinned_solc.sh "$SOLC_INSTALL_DIR" | tail -1)" >> "$GITHUB_ENV" + - name: Ensure SRS assets + run: scripts/ensure_srs_assets.sh + - name: IVC trace equivalence + run: scripts/run_ivc_bench.sh --trace --skip-srs-download + + lint: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: 1.90.0 + components: rustfmt, clippy + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + - name: Rust format + run: cargo fmt --all -- --check + - name: Clippy + run: cargo clippy --workspace --all-features --all-targets -- -D warnings diff --git a/proofs/solidity-verifier/.gitignore b/proofs/solidity-verifier/.gitignore index f49938ec9..d71bbacf3 100644 --- a/proofs/solidity-verifier/.gitignore +++ b/proofs/solidity-verifier/.gitignore @@ -1,2 +1,22 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +.vscode +generated/ +.srs/ +.solc/ + /cache/ /out/ diff --git a/proofs/solidity-verifier/Cargo.toml b/proofs/solidity-verifier/Cargo.toml new file mode 100644 index 000000000..964184904 --- /dev/null +++ b/proofs/solidity-verifier/Cargo.toml @@ -0,0 +1,94 @@ +[package] +name = "halo2_solidity_verifier" +version = "0.1.0" +edition = "2021" + +[dependencies] +# Depend on the Midfall proof stack from this workspace. This keeps generated +# Solidity and the native verifier tests pinned to the branch under test. +midnight-proofs = { path = "..", default-features = false, features = ["keccak-transcript", "committed-instances", "circuit-params"] } +midnight-curves = { path = "../../curves" } +ff = { workspace = true } +group = { workspace = true } +askama = { version = "0.12.0", features = ["config"], default-features = false } +hex = "0.4.3" +ruint = "1" +sha3 = { workspace = true } +itertools = "0.11.0" + +# For feature = "evm" +# +# revm 19+ ships a Prague handler with the EIP-2537 BLS12-381 precompiles +# (G1ADD = 0x0b, G1MSM = 0x0c, G2ADD = 0x0d, G2MSM = 0x0e, PAIRING = 0x0f). +# Because default features are disabled, enable `blst` explicitly; otherwise +# Prague does not register those BLS precompiles and staticcalls hit empty +# accounts instead of doing curve arithmetic. +revm = { version = "19", default-features = false, features = ["std", "blst"], optional = true } + +[dev-dependencies] +proptest = "1.6.0" +rand = { workspace = true } +revm = { version = "19", default-features = false, features = ["std", "blst"] } +rand_chacha = { workspace = true } +# Step 8: pull in midnight-circuits + midnight-zk-stdlib only as dev-deps so +# the lib build stays minimal. These provide the PoseidonExample circuit +# type the poseidon fixture (midfall/proofs/solidity-verifier/fixtures/ +# poseidon/{proof.bin, vk.bin, instance.be}) was generated against, which +# we need to deserialize the VK and rebuild the SolidityGenerator. +midnight-circuits = { path = "../../circuits", features = ["testing"] } +midnight-zk-stdlib = { path = "../../zk_stdlib", features = ["testing"] } +# IVC Poseidon-chain final-step Keccak proof end-to-end test +# (tests/ivc_keccak_solidity.rs). +midnight-aggregation = { path = "../../aggregation", features = [ + "keccak-transcript", + "truncated-challenges", +] } + +[features] +default = [] +evm = ["dep:revm"] +# Enables Askama trace/log branches in the default Solidity render path. +# `RenderDiagnostics { trace: true, .. }` can still force trace output without +# this feature. +solidity-trace = [] +# Enables LOG1 gas() checkpoints at section boundaries in the default +# Solidity render path. The host-side test parses the emitted logs into +# per-section gas deltas. Adds ~12 kg measurement overhead (16 LOG1 +# events * ~750 gas each); off in production builds. +solidity-gas-checkpoints = [] +# Mirrors midnight-proofs/truncated-challenges. When enabled, the +# rendered verifier: +# 1. Truncates `x3` (the f_com evaluation point) to its lower 128 +# bits immediately after squeezing. +# 2. Truncates each `x1` power (used as MSM scalars for q_com) and +# each `x4` power (used for the final commitment fold) to 128 +# bits at the point of use; the internal full-precision +# accumulator is preserved so power i+1 keeps full entropy. +# Required for proofs from a midnight-proofs prover compiled with +# matching feature settings; using the wrong setting on either side +# silently produces invalid pairings. +truncated-challenges = [ + "midnight-proofs/truncated-challenges", + "midnight-aggregation/truncated-challenges", +] +# Enables fewer-point-sets inside recursive verifier circuits and for the +# leaf proofs they consume. The final Solidity-facing proof can still disable +# proof-system dummy queries at runtime. +in-circuit-fewer-point-sets = ["midnight-aggregation/in-circuit-fewer-point-sets"] +# Enables fewer-point-sets for the final Solidity-facing proof layout. +outer-fewer-point-sets = ["midnight-proofs/fewer-point-sets"] +# Enables the Midnight single-H quotient commitment layout for the final +# Solidity-facing proof only. Do not forward this to midnight-circuits, +# midnight-zk-stdlib, or midnight-aggregation: recursive proofs verified +# inside the decider circuit stay on their existing multi-limb layout. +outer-single-h-commitment = ["midnight-proofs/single-h-commitment"] +# Backwards-compatible aggregate feature: old commands that pass +# `fewer-point-sets` retain the previous "everything on" behavior. +fewer-point-sets = ["in-circuit-fewer-point-sets", "outer-fewer-point-sets"] +# Enables native Midfall verifier trace hooks used by the Rust/Solidity +# differential coverage tests. +rust-verifier-trace = ["midnight-proofs/solidity-verifier-trace", "truncated-challenges"] + +[[example]] +name = "ivc_replay" +required-features = ["evm", "truncated-challenges", "fewer-point-sets"] diff --git a/proofs/solidity-verifier/LICENSE b/proofs/solidity-verifier/LICENSE new file mode 100644 index 000000000..245499ecb --- /dev/null +++ b/proofs/solidity-verifier/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Privacy & Scaling Explorations (formerly known as appliedzkp) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/proofs/solidity-verifier/README.md b/proofs/solidity-verifier/README.md new file mode 100644 index 000000000..6602487de --- /dev/null +++ b/proofs/solidity-verifier/README.md @@ -0,0 +1,399 @@ +# Halo2 Solidity Verifier + +> âš ï¸ This repo has NOT been audited and is NOT intended for a production environment yet. + +Solidity verifier generator for `midnight-proofs` / Midfall verifier proofs with +the KZG polynomial commitment scheme on BLS12-381. Generated verifiers use +Solidity `^0.8.24` source pragmas, and reproducible test/bench bytecode is +compiled with pinned `solc 0.8.30+commit.73712a01`. + +For audited solidity verifier generator and proof aggregation toolkits, please refer to [`snark-verifier`](http://github.com/axiom-crypto/snark-verifier). + +## Usage + +### Generate verifier and verifying key separately as 2 Solidity contracts + +```rust +let generator = SolidityGenerator::new(¶ms, &vk, GeneratorConfig::new(num_instances, 1)); +let artifacts = generator + .render(RenderOptions { + vk: RenderVk::Separate, + ..RenderOptions::default() + }) + .unwrap(); +let verifier_solidity = artifacts.verifier; +let vk_solidity = artifacts.verifying_key.unwrap(); +``` + +### Generate verifier and verifying key in a single solidity contract + +```rust +let generator = SolidityGenerator::new(¶ms, &vk, GeneratorConfig::new(num_instances, 1)); +let verifier_solidity = generator.render(RenderOptions::default()).unwrap().verifier; +``` + +### Encode proof into calldata to invoke `verifyProof` + +```rust +let calldata = generator.encode_calldata(&native_proof, &instances)?; +``` + +Note that function selector is already included. + +## Test + +### Current status + +As of this revision, +`cargo test -p halo2_solidity_verifier --all-features --all-targets -- --list` +reports 167 library tests and 4 integration tests. + +The implemented suite is narrower than the full assurance roadmap in +[`TESTING_STRATEGY.md`](./TESTING_STRATEGY.md). Today it covers transcript +compatibility, protocol planning, memory layout, VK payload encoding, proof +layout/canonicality checks, Solidity render invariants, Prague EIP-2537 smoke +coverage, Poseidon verifier property/adversarial tests, a Poseidon end-to-end +fixture, the Keccak IVC Poseidon-chain Solidity bench, and two ignored diagnostic +decompression probes. + +| Area | Current status | Default behavior | +| ---- | -------------- | ---------------- | +| Library/codegen tests under `src/` | Implemented and part of the normal Cargo suite. | Run by `cargo test -p halo2_solidity_verifier`. | +| Solidity/EVM tests in `src/test.rs` | Implemented behind the `evm` feature. Heavy Poseidon cases self-skip unless `HALO2_SOLIDITY_RUN_EVM_TESTS=1`, `solc`, and SRS assets are available. | Listed by Cargo; skipped cleanly without the gate. | +| `tests/poseidon_fixture.rs` | Implemented end-to-end Poseidon proof -> Solidity render -> `solc` -> Prague `revm` verification. | Self-skips unless `HALO2_SOLIDITY_RUN_EVM_TESTS=1`. | +| `tests/ivc_keccak_solidity.rs` | Implemented slow Keccak IVC final-proof Solidity bench over Poseidon hash-chain leaves. | Self-skips unless `HALO2_SOLIDITY_RUN_IVC_BENCH=1`. | +| `tests/fcom_decompress.rs` | Implemented diagnostic probes only. | Marked `#[ignore]`. | + +### Requirements + +The workspace is pinned to the toolchain in +[`rust-toolchain.toml`](./rust-toolchain.toml). The verifier now builds against +the surrounding Midfall workspace crates through local path dependencies. +Solidity-touching tests require `solc 0.8.30+commit.73712a01` on `PATH` or via +the `SOLC` environment variable. + +The heavy proof/EVM tests also need Filecoin/Midnight SRS assets. Set +`SRS_DIR` when they are not available at the default in-tree Midfall asset path: + +```bash +SRS_DIR=/path/to/midfall/zk_stdlib/examples/assets +``` + +Unless noted otherwise, run the commands below from the current Midfall +repository root. Package-scoped commands use `-p halo2_solidity_verifier` so +they exercise this verifier crate without accidentally selecting unrelated +workspace members. + +### Common commands + +List the currently implemented tests without running them: + +```bash +cargo test -p halo2_solidity_verifier --all-features --all-targets -- --list +``` + +Run the normal verifier suite. Without opt-in environment gates, heavy EVM and +IVC tests are still compiled/listed but skip at runtime: + +```bash +cargo test -p halo2_solidity_verifier --all-features --all-targets -- --nocapture +``` + +> [!NOTE] +> CI and reproducible benches compile Solidity with +> `solc 0.8.30+commit.73712a01`. + +The pinned Midfall revision, solc version, canonical IVC bench command, and +published verifier/VK/quotient runtime hashes are recorded in +[`docs/reference/REPRODUCIBLE_BUILDS.md`](./docs/reference/REPRODUCIBLE_BUILDS.md). +The bounded correctness and security claim, artifact manifest, threat model, +and release gates are in +[`docs/audit/CODEGEN_ASSURANCE_DOSSIER.md`](./docs/audit/CODEGEN_ASSURANCE_DOSSIER.md). +The compact reviewer handoff packet is in +[`docs/audit/REVIEW_PACKET.md`](./docs/audit/REVIEW_PACKET.md). +For a high-level architecture and lowering pipeline specification, see +[`docs/architecture/LOWERING_ARCHITECTURE_SPEC.md`](./docs/architecture/LOWERING_ARCHITECTURE_SPEC.md). +The map between the Askama templates, generated Solidity/Yul, Midfall Rust +verifier, and optimization choices is documented in +[`docs/reference/ASKAMA_TEMPLATE_RUST_MAPPING.md`](./docs/reference/ASKAMA_TEMPLATE_RUST_MAPPING.md). + +Run one test by name: + +```bash +cargo test -p halo2_solidity_verifier --all-features \ + lowering::layout::memory::tests::overlapping_permanent_regions_fail -- --nocapture +``` + +The maintained example is `examples/ivc_replay.rs`; obsolete legacy diagnostic +examples have been removed so default example discovery stays green. + +Run the `src/test.rs` Solidity/EVM verifier tests with the opt-in gate: + +```bash +HALO2_SOLIDITY_RUN_EVM_TESTS=1 \ +SRS_DIR=/path/to/midfall/zk_stdlib/examples/assets \ +cargo test -p halo2_solidity_verifier --release \ + --features evm,rust-verifier-trace --lib test:: -- --nocapture +``` + +Run a single Solidity/EVM test: + +```bash +HALO2_SOLIDITY_RUN_EVM_TESTS=1 \ +SRS_DIR=/path/to/midfall/zk_stdlib/examples/assets \ +cargo test -p halo2_solidity_verifier --release \ + --features evm,rust-verifier-trace \ + pbt_solidity_rejects_malleated_proofs -- --nocapture +``` + +Run the Poseidon integration fixture: + +```bash +HALO2_SOLIDITY_RUN_EVM_TESTS=1 \ +SRS_DIR=/path/to/midfall/zk_stdlib/examples/assets \ +cargo test -p halo2_solidity_verifier --release \ + --features evm,truncated-challenges --test poseidon_fixture -- --nocapture +``` + +Run the ignored diagnostic decompression probes: + +```bash +cargo test -p halo2_solidity_verifier --features evm \ + --test fcom_decompress -- --ignored --nocapture +``` + +### IVC detailed bench + +Run the Keccak IVC Solidity verifier bench over Poseidon hash-chain leaves with per-section gas checkpoints: + +```bash +SRS_DIR=/path/to/midfall/zk_stdlib/examples/assets \ +proofs/solidity-verifier/scripts/run_ivc_bench.sh +``` + +This prints the detailed checkpoint table, deployed runtime sizes, total +transaction gas, and real checkpointed section work. The default path uses the +multi-limb quotient commitment layout for the Solidity-facing decider proof, so +`SRS_DIR` must contain `midnight-srs-2p19` and `midnight-srs-2p20`. + +To benchmark the outer single-H quotient commitment layout, opt in explicitly: + +```bash +proofs/solidity-verifier/scripts/run_ivc_bench.sh --outer-single-h-commitment +``` + +That profile also needs `midnight-srs-2p22`. If `--skip-srs-download` is passed +and `2p22` is missing, the script fails before compiling; run without +`--skip-srs-download` once, or fetch it with: + +```bash +mkdir -p zk_stdlib/examples/assets +curl -fL --retry 3 --retry-delay 2 \ + -o zk_stdlib/examples/assets/midnight-srs-2p22 \ + https://srs.midnight.network/midnight-srs-2p22 +``` + +Single-H is outer-only in this repo: the recursive leaf proofs verified inside +the decider circuit stay on Midfall's multi-limb quotient layout. The opt-in +single-H bench therefore performs a two-phase run: first it generates a +multi-limb leaf bundle without `outer-single-h-commitment`, then it proves and +verifies the final decider proof with `outer-single-h-commitment`. + +The gas effect is intentionally modest. For the current IVC decider shape, +single-H removes three quotient commitment terms from the fused PCS final MSM: +`78 -> 75` terms. The exact gas delta depends on whether trace logs and +checkpoint logs are enabled; compare a local default run against +`--outer-single-h-commitment` for the current profile. The larger structural +effect is proof layout size: three fewer G1 commitments means `144` fewer +compressed proof bytes and `384` fewer EIP-2537-padded proof bytes. + +The runner keeps the recursive verifier on fewer point sets, but the outer +Solidity-facing decider proof does not enable `outer-fewer-point-sets`. + +Native Rust/Solidity trace equivalence is enabled by the trace-only bench path: +`proofs/solidity-verifier/scripts/run_ivc_bench.sh --trace --no-gas-checkpoints`. +Custom Midfall overrides must expose the +`midnight-proofs/solidity-verifier-trace` feature for that leg. +The IVC bench renders a pinned `Halo2QuotientEvaluator` alongside +`Halo2Verifier`, so the trace comparison includes quotient identity trace ids +`30_000..40_000` along with proof scalar reads, `q_evals`, and the reconstructed +quotient numerator. + +Generated IVC contracts and calldata are written inside the current Midfall +repository. Each IVC bench run overwrites this directory with the artifacts for +the most recent variant: + +```text +proofs/solidity-verifier/target/ivc-keccak-solidity-dump/ + Halo2Verifier.sol + Halo2VerifyingKey.sol + Halo2QuotientEvaluator.sol + calldata.bin + proof.bin + instance.le + contract-sizes.txt +``` + +Compile-check the IVC bench without running the full proof: + +```bash +proofs/solidity-verifier/scripts/run_ivc_bench.sh --check-only +``` + +For local, non-reproducible bench runs with whatever `solc` and Rust toolchain +are already available, opt out of the pins explicitly: + +```bash +proofs/solidity-verifier/scripts/run_ivc_bench.sh \ + --allow-unpinned-solc \ + --rust-toolchain stable +``` + +This keeps the SRS/download and bench flow the same, but generated bytecode and +gas numbers should be treated as ad hoc measurements rather than release +numbers. + +### Moonlight IVC wrap proof + +With a compatible `../Moonlight` checkout, run the Moonlight wrap-recursion +Solidity verifier example from the Midfall root. The dump directory is set +explicitly so the generated contracts stay in this Midfall repository: + +```bash +MOONLIGHT_RUN_WRAP_SOLIDITY_BENCH=1 \ +MOONLIGHT_RUN_WRAP_SOLIDITY_TRACE=1 \ +MOONLIGHT_WRAP_SOLIDITY_DUMP_DIR="$PWD/proofs/solidity-verifier/target/moonlight-wrap-solidity-dump" \ +SRS_DIR="$PWD/zk_stdlib/examples/assets" \ +cargo test --manifest-path ../Moonlight/aggregation/Cargo.toml \ + wrap_circuit_composes_two_fold_children_from_four_dummy_fold_proofs \ + --release -- --ignored --nocapture +``` + +To run the same Moonlight bench with unpinned local tooling: + +```bash +HALO2_SOLIDITY_ALLOW_UNPINNED_SOLC=1 \ +RUSTUP_TOOLCHAIN=stable \ +MOONLIGHT_RUN_WRAP_SOLIDITY_BENCH=1 \ +MOONLIGHT_RUN_WRAP_SOLIDITY_TRACE=1 \ +MOONLIGHT_WRAP_SOLIDITY_DUMP_DIR="$PWD/proofs/solidity-verifier/target/moonlight-wrap-solidity-dump" \ +SRS_DIR="$PWD/zk_stdlib/examples/assets" \ +cargo test --manifest-path ../Moonlight/aggregation/Cargo.toml \ + wrap_circuit_composes_two_fold_children_from_four_dummy_fold_proofs \ + --release --lib -- --ignored --nocapture +``` + +Compile-check the same Moonlight target without producing the proof: + +```bash +MOONLIGHT_RUN_WRAP_SOLIDITY_BENCH=1 \ +MOONLIGHT_RUN_WRAP_SOLIDITY_TRACE=1 \ +MOONLIGHT_WRAP_SOLIDITY_DUMP_DIR="$PWD/proofs/solidity-verifier/target/moonlight-wrap-solidity-dump" \ +SRS_DIR="$PWD/zk_stdlib/examples/assets" \ +cargo test --manifest-path ../Moonlight/aggregation/Cargo.toml \ + wrap_circuit_composes_two_fold_children_from_four_dummy_fold_proofs \ + --release --no-run +``` + +Generated Moonlight wrap contracts and calldata are written inside the current +Midfall repository. Each Moonlight wrap run overwrites this directory: + +```text +proofs/solidity-verifier/target/moonlight-wrap-solidity-dump/ + Halo2Verifier.sol + Halo2VerifyingKey.sol + calldata.bin + proof.bin + instance.le +``` + +Moonlight wrap can look slightly cheaper than the local IVC Keccak bench even +when both runs have the same verifier proof scale. In one representative run, +both had `102` proof eval scalars, `0` dummy PCS evals, `4` PCS point sets, and +a `5056` byte compressed proof, but Moonlight used `1,295,166` gas versus +`1,301,504` gas for the IVC bench. The main difference is topology: Moonlight +renders the quotient numerator inside the verifier, while the IVC bench uses a +pinned external `Halo2QuotientEvaluator`. That split keeps the main verifier +smaller, but checkpoint 12 pays the external-call/copy/return framing; in that +run, numerator reconstruction was `302,064` gas for Moonlight and `313,374` gas +for IVC. Moonlight gives some of that back through a larger public-input shape +(`19` wrap instance fields versus `14` IVC fields), which shows up in +`Lagrange + instance evaluation`. + +There is intentionally no single "all features plus all gated benches" command: +`outer-single-h-commitment` changes the final-proof SRS requirements and can be +covered with +`proofs/solidity-verifier/scripts/run_ivc_bench.sh --outer-single-h-commitment`. +To run the full package suite, the gated EVM tests, ignored diagnostics, and the +IVC bench, use the targeted commands above. The closest single Cargo sweep is: + +```bash +cargo test -p halo2_solidity_verifier --release --all-features --all-targets \ + -- --include-ignored --nocapture +``` + +That sweep compiles the all-features layout and runs ignored diagnostics, while +the heavy EVM and IVC proof paths remain covered by their explicit opt-in +commands. + +### Implemented test inventory + +The authoritative inventory is generated by: + +```bash +cargo test -p halo2_solidity_verifier --all-features --all-targets -- --list +``` + +The current output contains: + +- 177 library tests covering codegen planning, memory layout, PCS/query + planning, proof layout, quotient VM lowering, Solidity template invariants, + transcript compatibility, and gated Solidity/EVM verifier behavior. +- `tests/fcom_decompress.rs`: `fcom_decompress` and + `quotient_limb_decompress` (`#[ignore]` diagnostics). +- `tests/ivc_keccak_solidity.rs`: `ivc_final_keccak_solidity_e2e`. +- `tests/poseidon_fixture.rs`: `poseidon_renders_compiles_and_verifies`. + +## Limitations & Caveats + +- It currently supports the Midfall verifier shape used by this repo: exactly + one committed identity instance column and one non-committed public-input + column, no rotated instance queries, and KZG on BLS12-381. +- Verifying-key generation must be reproducible for the circuit shape being + rendered. If selector assignments can differ between proving and verifier + generation, disable selector compression or use the Midfall keygen path that + preserves the same selector layout. + +## Compatibility + +The generated transcript parser in +[`TranscriptProofParser.yul`](./templates/partials/verifier/TranscriptProofParser.yul) +follows the Midfall Keccak transcript shape used by the generated Solidity +verifier. + +## Design Rationale + +The current solidity verifier generator within `snark-verifier` faces a couple of issues: + +- The generator receives only unoptimized, low-level operations, such as add or mul. As a result, it currently unrolls all assembly codes, making it susceptible to exceeding the contract size limit, even with a moderately sized circuit. +- The existing solution involves complex abstractions and APIs for consumers. + +This repository is a ground-up rebuild, addressing these concerns while maintaining a focus on code size and readability. Remarkably, the gas cost is comparable, if not slightly lower, than the one generated by `snark-verifier`. + +See [`docs/reference/PR17_BORROWED_IDEAS.md`](./docs/reference/PR17_BORROWED_IDEAS.md) for the +small artifact-layout and packed-program ideas borrowed from +`privacy-ethereum/halo2-solidity-verifier` PR #17, and why this repo keeps a +static pinned verifier instead of adopting a fully reusable runtime artifact. + +See [`docs/architecture/MEMORY_LAYOUT.md`](./docs/architecture/MEMORY_LAYOUT.md) for the generated +verifier memory planner, including the fixed theta-relative offsets, +precompile-frame constants, scratch lifetimes, and update rules. + +See [`docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md`](./docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md) +for a consolidated specification and architecture guide covering the ABI, +proof layout, transcript, quotient reconstruction, KZG PCS check, VK payload, +and split verifier contracts. + +## Acknowledgement + +The template is heavily inspired by Aztec's [`BaseUltraVerifier.sol`](https://github.com/AztecProtocol/barretenberg/blob/4c456a2b196282160fd69bead6a1cea85289af37/sol/src/ultra/BaseUltraVerifier.sol). diff --git a/proofs/solidity-verifier/askama.toml b/proofs/solidity-verifier/askama.toml new file mode 100644 index 000000000..44f2f2f11 --- /dev/null +++ b/proofs/solidity-verifier/askama.toml @@ -0,0 +1,3 @@ +[[escaper]] +path = "askama::Text" +extensions = ["sol"] diff --git a/proofs/solidity-verifier/docs/architecture/ARCHITECTURE.md b/proofs/solidity-verifier/docs/architecture/ARCHITECTURE.md new file mode 100644 index 000000000..053ec88ec --- /dev/null +++ b/proofs/solidity-verifier/docs/architecture/ARCHITECTURE.md @@ -0,0 +1,194 @@ +# Architecture Notes + +This document records implementation techniques that affect the generated +Solidity verifier shape. `BENCH.md` keeps measured gas tables; this file +explains the design choices behind the code generator. + +## Compact Quotient Identity Interpreter + +The Halo2/Midnight verifier must evaluate the quotient numerator identities at +the Fiat-Shamir challenge `x`. The straightforward emitter renders every +identity as straight-line Yul: + +```yul +let v0 := mulmod(...) +let v1 := addmod(...) +... +``` + +That style is cheap to execute, but it makes the generated verifier source and +runtime bytecode grow with the number of identity operations. In the IVC Keccak +decider this quotient block was the dominant contract-size contributor. + +The compact interpreter changes only the representation of those same +arithmetic identities: + +1. The existing Rust evaluator still derives the identity expressions from the + circuit metadata and proof layout. +2. The code generator parses those Yul-like evaluator lines into expression + trees. +3. Repeated field constants are moved into a constant pool. +4. Expressions are encoded as dense bytecode opcodes. +5. The generated VK runtime stores the constant pool and packed quotient + bytecode. +6. The generated Solidity verifier includes a small Yul stack VM that reads + that pinned VK payload, executes the bytecode, and folds each identity into + `quotient_eval_numer` or a simple-selector accumulator. + +The verifier semantics do not change. The transcript, proof format, public +inputs, PCS checks, and final pairing checks are the same. The tradeoff is +explicit: smaller deployed code, more interpreter overhead. + +### Memory Model + +The interpreter uses three generated memory regions: + +- `stack_mptr`: temporary stack for expression evaluation. +- `const_mptr`: table of pooled `Fr` constants. +- `program_mptr`: packed quotient bytecode. + +`const_mptr` and `program_mptr` point inside the VK runtime bytes that the +verifier already copied with `extcodecopy`. The permanent memory layout places +the challenge region after the full VK runtime, so the quotient payload remains +available until the quotient interpreter executes. + +Only the VM stack is scratch memory. It is placed after the proof +commitment/evaluation regions so it does not clobber VK data, transcript +challenges, PCS scratch space, or selector accumulators. + +### Pinned VK Payload + +Earlier compact-interpreter versions embedded the quotient constant pool and +program in the verifier as many `PUSH32` + `mstore` immediates. That kept the VK +small, but it left the verifier just above the 24KB EIP-170 runtime limit. + +The current generator moves that static payload into `Halo2VerifyingKey.sol`. +The verifier still pins the VK by both runtime length and codehash, so calldata +cannot redirect or mutate the program. This is a size split, not a trust-model +change: the quotient program is still generated from the same circuit metadata, +and a verifier deployment accepts exactly one VK runtime hash. + +This pinned-artifact model is the reason later code-size optimizations may +specialize the quotient interpreter itself. The verifier is not intended to +accept arbitrary VK runtimes at a single deployed address; it accepts one +generated VK runtime hash. Therefore the rendered VM can omit opcode and memory +token switch arms that the finalized pinned bytecode never uses. That is a +runtime-size optimization over the generated artifact, not a change to the +Rust verifier semantics. + +### Opcode Strategy + +The base VM supports compact forms for: + +- pushing pooled constants; +- pushing memory-backed proof/VK/evaluation words; +- challenge and Lagrange memory tokens; +- `addmod`, `mulmod`, and negation; +- folding one completed identity into the quotient accumulator; +- folding one completed identity into a simple-selector accumulator. + +Two important optimizations sit on top of the base VM. + +#### Top-Of-Stack Cache + +The Yul interpreter keeps the current stack top in a local variable. Push +operations spill the old top to memory only when there is already a cached +value. Binary operations pop one value from memory and combine it with the +cached top. + +This reduces repeated `mstore`/`mload` traffic without changing the bytecode +format for normal opcodes. + +#### Product-Add Macro Opcodes + +The IVC quotient identities contain long runs of this shape: + +```text +acc += mem_a * mem_b * const +``` + +Encoding that as primitive VM bytecode costs several dispatches: + +```text +push mem_a +mul mem_b +mul const +add +``` + +The generator recognizes product leaves and emits macro opcodes such as: + +- `add_mul_mem_mem_const_u8` +- `add_mul_const_u8_mem_u16` +- `add_mul_mem_mem` + +The interpreter executes each macro with one dispatch, while still using the +same `addmod` and `mulmod` arithmetic. Repeated adjacent macro terms are then +packed into run opcodes, so a long sequence of product-add terms is interpreted +by one outer dispatch and a tight Yul loop over packed operands. + +This is the main gas recovery technique after moving from straight-line Yul to +an interpreter. + +### Current IVC Result + +The two-leaf IVC Keccak decider bench verifies end to end on-chain with this +interpreter and macro encoding. + +Latest measured result: + +```text +Halo2Verifier.sol source bytes: 94,591 +Halo2VerifyingKey.sol source bytes: 60,263 +Halo2QuotientEvaluator.sol source bytes: 162,152 +Halo2Verifier deployed runtime bytes: 12,061 +Halo2VerifyingKey deployed runtime bytes: 14,016 +Halo2QuotientEvaluator runtime bytes: 23,221 +total deployed runtime bytes: 49,298 + +compressed proof bytes: 5,056 +EIP-2537 padded proof bytes: 7,776 +calldata bytes: 8,356 + +quotient numerator gas: 412,748 +PCS final MSM gas: 533,202 +total tx gas: 1,399,196 +``` + +Compared to the first compact interpreter commit, the product-add and run macro +layer reduced the quotient section from `2,578,177` gas to `1,045,546` gas and +the verifier runtime from `30,278` bytes to `25,598` bytes. + +Moving the quotient payload into the pinned VK then reduced the verifier +runtime from `25,598` bytes to `11,432` bytes. The VK grew from `6,752` bytes to +`19,712` bytes, so both deployable contracts are now independently below the +24KB EIP-170 limit. + +The latest default also enables limb-aware quotient VM opcodes. In the current +IVC VK they compress `14` seven-limb linear forms and `7` seven-limb row +products, saving about `13k` gas in the numerator checkpoint while keeping each +runtime artifact below the EIP-170 limit. + +### Validation Commands + +Small Solidity smoke: + +```sh +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ +cargo test --features evm,truncated-challenges,fewer-point-sets,solidity-gas-checkpoints \ + --test poseidon_fixture poseidon_renders_compiles_and_verifies \ + -- --ignored --nocapture +``` + +Full IVC Solidity bench: + +```sh +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ +./scripts/run_ivc_bench.sh --skip-srs-download +``` + +Generated artifacts are written to: + +```text +target/ivc-keccak-solidity-dump/ +``` diff --git a/proofs/solidity-verifier/docs/architecture/LOWERING_ARCHITECTURE_SPEC.md b/proofs/solidity-verifier/docs/architecture/LOWERING_ARCHITECTURE_SPEC.md new file mode 100644 index 000000000..2b1fea580 --- /dev/null +++ b/proofs/solidity-verifier/docs/architecture/LOWERING_ARCHITECTURE_SPEC.md @@ -0,0 +1,610 @@ +# Halo2 Solidity Verifier Architecture And Lowering Specification + +## 1. Purpose + +This repository generates Solidity verifiers for a constrained Halo2 verifier +profile used by the Midnight/Midfall stack. The output is not a generic Halo2 +verifier generator. It targets a specific proof shape, transcript, polynomial +commitment scheme, and on-chain execution model: + +- Halo2 KZG proofs over BLS12-381. +- Midfall/Midnight proof layout and Fiat-Shamir transcript semantics. +- Solidity/Yul verifiers that use the EIP-2537 BLS12-381 precompiles. +- A proof calldata representation suitable for Ethereum ABI transport. +- Optional support for public accumulator verification at the end of the proof. +- Optional trace and gas-checkpoint builds for debugging and benchmarking. + +The main user-facing API is `SolidityGenerator`. Given KZG parameters, a +verifying key, and the public instance shape, it validates the circuit profile, +derives a deterministic verifier plan, renders Solidity from Askama/Yul +templates, and provides helper APIs to repack native Halo2 proof bytes into the +generated verifier ABI. + +For the audit-facing correctness and security argument, use +[`CODEGEN_ASSURANCE_DOSSIER.md`](./CODEGEN_ASSURANCE_DOSSIER.md). This +architecture document explains how the system is built; the dossier states the +bounded claim, artifact manifest, threat model, and required evidence gates. + +## 2. Supported Protocol Envelope + +The generator intentionally accepts only the protocol shape it knows how to +verify on-chain. + +The verifier currently assumes: + +- Exactly one committed instance column and one non-committed public input + column. +- No rotated instance queries. +- At least one advice column. +- Midfall-compatible KZG transcript behavior. +- BLS12-381 field and curve encodings used by the EIP-2537 precompile ABI. +- A proof layout whose commitment, evaluation, quotient, and accumulator reads + can be derived statically from the verifying key metadata. + +When accumulator support is enabled, the public input tail is expected to carry +a fully collapsed BLS12-381 accumulator in radix-2^56 limbs: + +- Left-hand side point coordinates and scalar. +- Right-hand side point coordinates and scalar. +- Seven 56-bit limbs per BLS base-field coordinate. +- Four limbs packed into each public-input field element. +- Ten public-input field elements for the collapsed accumulator words. + +This validation is done before rendering. Unsupported circuit shapes should +fail during generator construction instead of producing verifier code that +fails later on-chain. + +## 3. Repository Architecture + +The codebase is organized around a small public API and a deeper codegen +pipeline. + +| Path | Responsibility | +| --- | --- | +| `src/lib.rs` | Public exports, feature flags, calldata helpers, EVM helpers. | +| `src/api.rs` | Public configuration and diagnostic types, supported-shape errors, accumulator encoding metadata. | +| `src/builder/` | Thin `SolidityGenerator` facade, constructor validation, public render/calldata/diagnostic wrappers. | +| `src/lowering/artifacts.rs` | Verifier, VK, and quotient evaluator artifact assembly. | +| `src/lowering/vk.rs` | Metadata convergence, VK payload generation, and static memory layout entrypoints. | +| `src/lowering/calldata.rs` | Proof repacking and Solidity calldata encoding plan. | +| `src/lowering/quotient.rs` | Quotient identity planning, selector folds, and generated quotient blocks. | +| `src/lowering/protocol/` | Typed protocol plan derived from the verifying key. | +| `src/lowering/abi/` | Static proof calldata and transcript-buffer layout. | +| `src/lowering/render/` | Askama template data models and Yul formatting boundary. | +| `src/lowering/layout/` | Static Yul memory planner, VK payload layout, numeric layout facts, and overlap validation. | +| `src/lowering/kzg/` | KZG multi-open verifier emitter and pairing-input construction. | +| `src/lowering/quotient_numerator/yul_emit.rs` | Batched identity numerator reconstruction emitter. | +| `src/lowering/quotient_numerator/vm/` | Compact quotient numerator VM compiler and metadata. | +| `src/lowering/encoding/` | Pointer, word, field, curve, calldata, and Yul formatting helpers. | +| `templates/contracts/` | Generated Solidity contract templates consumed by Askama. | +| `templates/partials/` | Shared Solidity/Yul fragments included by contract templates. | +| `tests/` | End-to-end rendering, EVM, trace, gas, and compatibility tests. | +| `docs/` | Grouped architecture, audit, benchmark, plan, and reference material. | + +The public API deliberately hides most of the planning machinery. Users usually +construct a generator, render Solidity, repack a proof, and submit calldata to +the generated contract. Internally, rendering is a deterministic lowering pass +followed by template expansion. + +## 4. Public API Surface + +The central type is: + +```rust +let generator = SolidityGenerator::new( + ¶ms, + &vk, + GeneratorConfig::new(num_instances, num_committed_instances), +); +let artifacts = generator.render(RenderOptions { + vk: RenderVk::Separate, + ..RenderOptions::default() +})?; +``` + +The important render modes are: + +- `RenderVk::Embedded`: render one verifier with the VK embedded. +- `RenderVk::Separate`: render verifier plus separate verifying-key contract. +- `RenderDiagnostics { trace, gas_checkpoints }`: include trace hooks and/or + section gas checkpoints. +- `render_quotient_evaluator(diagnostics)`: render the external quotient + evaluator first so deployment tooling can compute its runtime length/hash. +- `RenderQuotient::ExternalPinned { runtime_len, codehash }`: render verifier, + optional VK, and quotient evaluator using a pinned evaluator artifact. + +The older unpinned external-quotient render APIs are deprecated and intentionally +panic. External evaluator deployment must be pinned because verifier and +evaluator bytecode must agree exactly on layout, program bytes, constants, and +ABI conventions. + +The proof helper API is: + +```rust +let calldata = generator.encode_calldata(&native_midfall_proof, &instances)?; +``` + +It converts Midfall/Halo2 proof bytes into the generated Solidity verifier ABI. +The helper performs length checks and applies the same proof-layout plan used by +codegen. + +## 5. End-To-End Codegen Pipeline + +At a high level, lowering is a sequence of pure planning steps that produce +template data, followed by Askama rendering. + +```mermaid +flowchart TD + A["KZG params + verifying key + instance shape"] --> B["SolidityGenerator validation"] + B --> C["Protocol plan"] + C --> D["Proof calldata layout"] + C --> E["PCS query plan"] + C --> F["Quotient numerator programs"] + D --> G["Proof repacker plan"] + E --> H["Template data"] + F --> I["VK payload sections"] + I --> J["Static memory layout"] + J --> H + G --> K["Solidity calldata"] + H --> L["Askama Solidity/Yul render"] + L --> M["Verifier, VK, optional quotient evaluator"] + K --> N["EVM verifyProof call"] + M --> N +``` + +1. Construct `SolidityGenerator`. +2. Validate the verifying-key and instance shape. +3. Build protocol metadata from the Halo2 constraint system. +4. Derive proof read order and proof calldata layout. +5. Derive verifying-key payload layout. +6. Compile compact quotient numerator programs and constants. +7. Compute static Yul memory layout and scratch-space lifetimes. +8. Compute PCS query sets and optional dummy queries. +9. Re-run metadata-dependent passes until sizes and addresses converge. +10. Render Solidity/Yul templates. +11. Repack native proof bytes into Solidity calldata. +12. Deploy the generated contracts and call the verifier entrypoint. + +The pipeline is intentionally layout-first. The generated Solidity uses absolute +Yul memory addresses and packed bytecode sections, so lowering must know +all proof, VK, and scratch regions before it can safely emit runtime code. + +The most important source-of-truth rule is: + +| Concern | Source of truth | +| --- | --- | +| Supported circuit and instance shape | `SolidityGenerator::try_new` and `ConstraintSystemMeta` | +| Proof read order | `ProtocolPlan` and `ProofCalldataLayout` | +| Solidity calldata offsets | `ProofCalldataLayout` and `RepackedProofLayoutPlan` | +| VK bytes and offsets | `VkPayloadLayout` | +| Scratch memory addresses | `VerifierMemoryLayout` | +| Quotient numerator bytecode | `QuotientProgramBuild` | +| PCS query order | `kzg::queries` planning over the protocol plan | +| Rendered syntax | Askama templates under `templates/` | + +## 6. Generator Construction And Validation + +`SolidityGenerator::try_new` is the validating constructor. It records: + +- KZG parameters. +- Verifying key. +- Public instance counts. +- Number of committed instance columns. +- Optional accumulator encoding. +- Derived constraint-system metadata. + +Validation is intentionally early. The generator rejects shapes that would +require runtime behavior the templates do not implement. Examples include +rotated instance queries, missing advice columns, or unsupported public-instance +layouts. + +`SolidityGenerator::new` wraps `try_new` and panics on validation failure. Tests +and applications that need recoverable errors should prefer `try_new`. + +## 7. Protocol Planning + +`src/lowering/protocol/mod.rs` provides the typed source of truth for the verifier +protocol. + +The planner derives: + +- Commitment read groups. +- Evaluation read groups. +- PCS query sources and order. +- Common polynomial requirements. +- Quotient identity topology. +- Which fixed, advice, instance, and challenge queries are used. +- Challenge and transcript sequencing implied by the proving system. + +This mirrors the shape of `snark-verifier`, but avoids relying on an EVM loader +or dynamic unrolled execution model. The result is a static plan that can be +consumed by Rust-side layout code and Askama/Yul templates. + +The key design rule is that proof reads are not scattered through templates. +Templates receive an already validated protocol plan, so proof order, transcript +absorbs, quotient identity structure, and PCS query order stay synchronized. + +## 8. Proof Layout And ABI + +`src/lowering/abi/proof.rs` converts the protocol plan into byte offsets. + +The layout replays the verifier's read order: + +1. Advice commitments by phase. +2. Lookup multiplicity commitments. +3. Permutation product commitments. +4. Lookup helper and accumulator commitments. +5. Trash commitments. +6. Quotient commitments. +7. Evaluations. +8. `f_com`. +9. Quotient evaluation sets. +10. Public input words. + +The generator then derives: + +- Total G1 commitment count. +- Total scalar/evaluation count. +- Group boundaries for transcript absorbs. +- Offsets used by the Solidity verifier. +- A `RepackedProofLayoutPlan` for native-proof to calldata conversion. + +The native compressed proof uses compact BLS12-381 encodings. The generated +Solidity verifier expects EIP-2537-style padded coordinates. During repacking, +compressed G1 commitments are decompressed into four 32-byte words: + +- `x_hi` +- `x_lo` +- `y_hi` +- `y_lo` + +Scalar words are converted into the big-endian field representation expected by +the generated verifier. The repacker also preserves the generator's exact +ordering for commitment groups, evaluations, quotient material, and public +inputs. + +## 9. Verifying-Key Artifact Layout + +`src/lowering/layout/vk_payload.rs` defines the typed payload layout for generated VK +artifacts. + +The VK payload is split into monotonic sections: + +- Header. +- Compact quotient constants. +- Compact quotient programs. +- Fixed commitments. +- Permutation commitments. + +The generator first reserves space for quotient constants and program bytes, +then compiles the compact quotient programs, fills the reserved sections, and +validates the resulting `VkPayloadLayout`. + +The compact quotient VM bytecode is packed into big-endian U256 words. Padding +is explicit and checked on decode, which keeps the Solidity-side byte slicing +predictable. + +Before bytecode is written into the VK payload, the generator re-decodes the +final physical program and validates opcode support, operand length, memory +tokens, stack depth, and identity-boundary emptiness. This keeps the lean Yul VM +from depending on unchecked generator assumptions. + +Separated verifier mode emits a verifier contract plus a VK contract. Embedded +mode emits one contract with the VK constants in the same artifact. The runtime +protocol is the same in both modes. + +## 10. Memory Model + +The generated verifier is written for predictable, low-overhead Yul execution. +It does not rely on Solidity's free-memory pointer for its main work areas. +Instead, `src/lowering/layout/memory.rs` assigns absolute memory ranges at lowering time. + +Each range has: + +- A symbolic name. +- A start address. +- A byte length. +- A lifetime phase. +- A reuse policy. + +The planner rejects overlapping ranges whose lifetimes can coexist. Scratch +regions may intentionally reuse the same addresses only when their phases are +disjoint. + +Important phases include: + +- Transcript. +- Quotient VM. +- Fixed PCS preparation. +- Final PCS MSM. +- PCS pairing. +- Accumulator MSM. +- Accumulator pairing batch. +- Final pairing. +- Verifier return. +- Quotient return. + +This static memory model is one of the central design constraints of the +codebase. If a template needs new scratch memory, it should be added to the +memory planner rather than hard-coded directly into Yul. + +## 11. Metadata Convergence + +Several generated sizes depend on earlier generated artifacts: + +- VK payload size depends on compact quotient constants and program bytes. +- Memory base addresses depend on VK payload size. +- PCS dummy query count depends on protocol metadata and feature flags. +- Template scratch requirements depend on finalized memory addresses. + +`generator.rs` handles these dependencies with bounded convergence loops. It +builds provisional metadata, derives sizes, rebuilds with the new sizes, and +checks that the stable static layout has converged. If section sizes do not +settle within the allowed iteration count, generation fails instead of emitting +ambiguous bytecode. + +## 12. Quotient Numerator Codegen + +The quotient system in `src/lowering/quotient/` compiles Halo2 quotient numerator +logic into a compact VM representation. + +The compiler: + +- Lowers gate, permutation, lookup, and trash identities. +- Assigns constants. +- Performs common-subexpression handling where supported. +- Emits bytecode and constant tables. +- Tracks maximum stack depth. +- Tracks memory tokens and native callback requirements. +- Preserves the identity stream order expected by Midfall. + +At runtime, the Yul quotient VM reconstructs the batched identity numerator. +The emitted verifier stores the negative numerator value as the expected opening +scalar, while the commitment side carries the `(1 - x^n)` factor. + +The codebase uses two related but distinct pieces: + +- `quotient/`: compiler for compact numerator programs. +- `evaluator.rs`: Yul emitter for batched identity numerator reconstruction and + linearization-related scalar preparation. + +The quotient evaluator is not an arbitrary Solidity subroutine. It is tied to a +specific proof layout, VK layout, transcript schedule, and quotient program +artifact. + +## 13. PCS Codegen + +`src/lowering/kzg/mod.rs` emits the KZG multi-open verifier logic. It mirrors the +Midfall `multi_prepare` flow in Yul. + +The generated PCS path: + +1. Builds the verifier query list. +2. Constructs intermediate point sets. +3. Sorts sets by ascending cardinality where required. +4. Precomputes rotation points `x * omega^rot`. +5. Computes powers of challenge `x1`. +6. Folds quotient evaluations into query commitments. +7. Materializes `q_com` inputs. +8. Computes `f_eval` by Lagrange interpolation at `x3`. +9. Computes the final commitment with `x4` powers, `f_com`, and `v`. +10. Constructs pairing inputs `(pi, final_com - vG + x3*pi)`. + +Feature flags can adjust this path. For example, fewer point sets can introduce +dummy evaluations so the generated verifier keeps a compact, stable query shape. + +## 14. Transcript And Challenge Flow + +The generated verifier follows the Midfall transcript order. Commitments, +instances, evaluations, quotient material, and accumulator material must be read +and absorbed in the same order used by proof generation. + +For Moonlight wrap proofs and IVC final proofs, the relevant integration path +uses the special Midfall Keccak transcript: + +```rust +CircuitTranscript +``` + +This matters because a proof generated with a different transcript hash will +not verify on-chain even if the circuit, VK, and calldata layout are otherwise +correct. The Solidity verifier assumes the Keccak transcript schedule emitted +by this codegen path. + +## 15. Generated Solidity Runtime + +The rendered Solidity contract is mostly a small ABI shell around Yul verifier +logic. + +Runtime execution follows this shape: + +1. Validate calldata shape. +2. Load or reference the verifying-key payload. +3. Prevalidate public accumulator material, when enabled, before transcript, + quotient, PCS, and final pairing work. +4. Initialize transcript memory. +5. Absorb VK digest, committed public input material, and instance material. +6. Read proof commitments and evaluations into the static proof layout. +7. Squeeze challenges in the expected order. +8. Reconstruct quotient numerator and linearization scalars. +9. Run PCS multi-open preparation. +10. Optionally batch the already-validated public accumulator equation into the + final pairing inputs. +11. Run final pairing checks through EIP-2537 precompiles. +12. Return the verifier result. + +Gas-checkpoint builds insert measurement points into this flow. Trace builds +insert diagnostic events or hooks. These modes should not change the verifier's +semantic result. + +## 16. Accumulator Verification + +Accumulator support is represented by `AccumulatorEncoding`. + +When enabled, the generator knows how to locate packed accumulator limbs in the +public input tail. The generated verifier unpacks this data and routes each +decoded carried point through EIP-2537 G1MSM near the start of verification, +then later batches the resulting accumulator pairing equation with the +verifier's KZG pairing path. + +Two public-input accumulator layouts are supported: + +- `AccumulatorEncoding::new(...)`: Midfall IVC-style accumulator input, + encoded as `lhs point, lhs scalar, rhs point, rhs scalar`, with an optional + fixed-base scalar tail. +- `AccumulatorEncoding::point_pair(...)`: already-collapsed Moonlight wrap + input, encoded as `lhs point, rhs point`; the generated verifier treats both + carried scalars as one and rejects fixed-base scalar tails for this layout. + +The accumulator layout is intentionally explicit because public input packing is +part of the on-chain ABI. Any change to limb count, limb width, point order, or +public-input placement must update both the Rust-side encoder and the generated +Solidity reader. + +## 17. Template System + +Templates live under `templates/` and are rendered through Askama. The Rust +side should do planning; templates should consume facts. + +That means templates generally receive: + +- Protocol read plans. +- Proof offsets. +- Memory addresses. +- VK section offsets. +- Quotient program metadata. +- PCS query metadata. +- EIP-2537 constants. +- Feature flags and trace/gas instrumentation settings. + +Templates should avoid rediscovering layout rules. When a template needs a new +address, byte offset, query count, or section length, add that concept to the +Rust-side model first. + +## 18. Feature Flags And Build Modes + +The public feature constants exported from `src/lib.rs` let tests and generated +artifacts know which codegen paths are active. + +Important modes include: + +- Solidity trace instrumentation. +- Solidity gas checkpoints. +- Truncated challenges. +- Outer fewer point sets. +- Outer single `h` commitment. +- EVM test support. + +Feature flags are part of the generated verifier's behavior. A proof produced +for one verifier shape may not be valid for a verifier rendered with a different +feature set. + +## 19. Testing And Bench Strategy + +The repository tests the generator through several layers: + +- Rust unit tests for layout, packing, and helper APIs. +- Solidity rendering tests. +- EVM deployment and verification tests. +- Trace builds for debugging challenge and proof-read mismatches. +- Gas checkpoint builds for section-level profiling. +- IVC and Moonlight integration benches for realistic proof shapes. + +Representative local commands: + +```bash +cargo test --lib +``` + +```bash +cargo test --features evm --test codegen +``` + +```bash +MOONLIGHT_RUN_WRAP_SOLIDITY_BENCH=1 \ +cargo test --manifest-path /Users/Julien.Coolen/Moonlight/aggregation/Cargo.toml \ + wrap_circuit_composes_two_fold_children_from_four_dummy_fold_proofs --release \ + --lib -- --ignored --nocapture +``` + +The Moonlight command runs from this repository's Solidity verifier integration +when Moonlight has a local path dependency pointing back to this checkout. It +generates Moonlight wrap proof material, repacks it for the Solidity verifier, +deploys the generated verifier path, and checks the proof on-chain in the local +EVM harness. + +## 20. Extension Guidelines + +When extending codegen, keep changes anchored in the planner before touching +templates. + +For a new proof read: + +1. Add it to the typed protocol plan. +2. Add offsets to `ProofCalldataLayout`. +3. Update the repacker. +4. Expose the offset through template data. +5. Update trace and gas-checkpoint builds. +6. Add an EVM or layout test. + +For a new VK section: + +1. Add a typed section to `VkPayloadLayout`. +2. Include it in payload convergence. +3. Validate section offsets and padding. +4. Update embedded and separated render modes. +5. Add decode or byte-layout tests. + +For new Yul scratch memory: + +1. Add a named range to `VerifierMemoryLayout`. +2. Assign the correct `MemoryPhase`. +3. Let the overlap checker validate reuse. +4. Thread the address through `template.rs`. +5. Avoid hard-coded addresses in templates. + +For quotient VM changes: + +1. Extend the opcode, token, or constant model in Rust. +2. Update the packed-program codec if byte encoding changes. +3. Update the physical-program safety validator. +4. Update the runtime Yul VM. +5. Add bytecode round-trip and safety tests. +6. Add an end-to-end verifier test for the affected identity shape. + +For transcript changes: + +1. Update the Rust-side transcript schedule. +2. Update Solidity/Yul absorbs and squeezes. +3. Update proof generation to use the same transcript. +4. Add a negative test showing mismatched transcripts fail. +5. Re-run a real proof bench, not only a rendering test. + +## 21. Design Invariants + +The implementation relies on these invariants: + +- Rust planning is the source of truth for layout. +- Templates render already planned data. +- Proof repacking and generated Solidity share one proof-layout plan. +- VK payload offsets are typed and validated. +- Yul memory addresses are planned and overlap-checked. +- External quotient evaluators are pinned to the verifier artifact. +- Transcript order must match proof generation exactly. +- EIP-2537 field and curve encodings are ABI, not implementation detail. +- Gas and trace instrumentation must not change verifier semantics. + +These invariants are more important than any individual template. Most verifier +bugs come from letting two parts of the stack make independent assumptions about +proof order, memory addresses, transcript absorbs, or public input packing. + +## 22. Related Documents + +This specification is the architectural overview. More focused details live in: + +- `docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md` +- `docs/architecture/MEMORY_LAYOUT.md` +- `docs/reference/ASKAMA_TEMPLATE_RUST_MAPPING.md` +- `docs/reference/QUOTIENT_NUMERATOR_EVALUATOR.md` +- `docs/reference/QUOTIENT_EVALUATOR_9KB_BYTECODE.md` +- `docs/reference/TEAM_DEMO_SETUP.md` diff --git a/proofs/solidity-verifier/docs/architecture/MEMORY_LAYOUT.md b/proofs/solidity-verifier/docs/architecture/MEMORY_LAYOUT.md new file mode 100644 index 000000000..4550d1607 --- /dev/null +++ b/proofs/solidity-verifier/docs/architecture/MEMORY_LAYOUT.md @@ -0,0 +1,272 @@ +# Generated Verifier Memory Layout + +This document explains the internal memory planner used by Solidity codegen. +The planner lives in `src/lowering/layout/memory.rs`. + +The generated verifier uses absolute Yul memory addresses. This is intentional: +it keeps verifier code compact, makes EIP-2537 and modexp precompile frames easy +to build, and lets the external quotient evaluator receive the same memory image +as the monolithic verifier. The cost is that memory safety has to be proven by +codegen, not by Solidity's free-memory pointer. + +The current planner is conservative. It preserves the existing generated +addresses, but the offsets are now derived by Rust compatibility manifests +(`VerifierMemoryLayout`, `ThetaWindowLayout`, and the proof/VK layout helpers) +before being rendered into Solidity/Yul. It is not a deterministic repacker +yet. + +## Solidity Memory Model Boundary + +Solidity reserves the first four words of memory for compiler conventions: + +- `0x00..0x3f`: scratch space; +- `0x40..0x5f`: the free-memory pointer; +- `0x60..0x7f`: the zero slot used as the initial value for dynamic memory + arrays; +- `0x80`: the initial allocatable memory pointer. + +The generated verifier intentionally does not follow Solidity allocation by +reading and bumping `mload(0x40)`. Instead, every generated absolute memory +region is planned at or above `0x80`. The streaming transcript buffer, the main +verifier return word, the split quotient return frame, the VK constructor +payload buffer, and low-memory precompile scratch all start from named +Rust-side layout constants rooted at `SOLIDITY_ALLOCATABLE_MEMORY_START`. + +The code generator treats `[0x00..0x80)` as off limits for generated writes: +`VerifierMemoryLayout::validate()` rejects any registered region inside that +reserved prefix, and template tests pin the remaining hand-written return and +scratch frames to `0x80` or above. + +The verifier still has a constrained generated-contract shape: + +1. `verifyProof` is an external entrypoint with `calldata` arguments, so the + proof and instances are not eagerly decoded into Solidity-managed memory. +2. The high-level Solidity work before the main verifier block is limited to + value-type dependency checks. Accepted executions then enter the generated + `assembly ("memory-safe")` body. +3. The main verifier assembly body is terminal: every path either reverts or + ends with `return(RETURN_MPTR, 0x20)`. +4. The split `Halo2QuotientEvaluator` fallback has its own fresh EVM memory + frame, copies the verifier frame into generated absolute addresses, writes + its compact return frame at `0x80`, and immediately returns. +5. The `Halo2VerifyingKey` constructor writes `INVALID || runtime payload` + starting at `0x80` and immediately returns that prefixed runtime as contract + code. The verifier later skips byte `0` and copies only the payload. + +Do not move the generated verifier assembly into a reusable internal Solidity +function, library routine, or wrapper that continues executing high-level +Solidity after the block without reviewing the absolute-memory strategy. Future +dynamic-allocation work should use `mload(0x40)`/`mstore(0x40, ...)` instead of +hard-coded high-water marks, but the current generator at least preserves +Solidity's reserved prefix. + +Relevant Solidity references: + +- Layout in memory: + +- Inline assembly memory safety: + +- Advanced safe use of memory: + +- EVM call memory is freshly cleared per message call: + + +## Planner APIs + +`MemoryArena` owns the memory map. It has three compatibility-mode allocation +primitives: + +| API | Purpose | +| --- | --- | +| `alloc_fixed(name, start, len, lifetime)` | Register a region at an explicit historical or precompile-required byte address. | +| `alloc_after(name, anchor_start, anchor_len, len, lifetime)` | Register a region immediately after an anchor range, rounded up to the next EVM word. | +| `alloc_phase_scratch(name, start, len, phase)` | Register fixed-address scratch that is live only during one `MemoryPhase`. | + +`ScratchAllocator` is rooted at a fixed base and keeps one cursor per phase. +Allocations in the same phase advance sequentially; allocations in different +phases intentionally reuse the base. That models the current verifier layout +without repacking it: + +```text +base = quotient_tmp_mptr + +QuotientVm: + quotient_temps -> base + quotient_stack -> base + quotient_temps_len + +PcsQEvalSourceTable: + q_eval source table -> base + +PcsQComTrace: + optional q_com trace MSM -> base + +PcsFinalMsm: + final MSM input -> base +``` + +## Units + +All planned regions are byte ranges, but every start and length must be aligned +to `WORD_BYTES == 0x20`. + +| Name | Value | Why | +| --- | ---: | --- | +| `WORD_BYTES` | `0x20` | EVM word size; all `mload`/`mstore` and Fr scalars are word-sized. | +| `FR_WORDS` / `FR_BYTES` | `1` / `0x20` | BLS12-381 Fr scalar as one canonical calldata/memory word. | +| `G1_WORDS` / `G1_BYTES` | `4` / `0x80` | EIP-2537 padded G1: `x_hi, x_lo, y_hi, y_lo`. | +| `G2_WORDS` / `G2_BYTES` | `8` / `0x100` | EIP-2537 padded G2: two Fp2 coordinates, two words per Fp limb. | +| `G1_MSM_PAIR_BYTES` | `0xa0` | G1MSM precompile input tuple: one G1 plus one scalar. | +| `G1ADD_INPUT_BYTES` | `0x100` | G1ADD precompile input tuple: two G1 points. | +| `MODEXP_FRAME_BYTES` | `0xc0` | 32-byte base/exp/mod frame: three length words plus three values. | +| `PAIRING_PAIR_BYTES` | `0x180` | Pairing tuple: one G1 plus one G2. | +| `PAIRING_TWO_PAIR_BYTES` | `0x300` | Final KZG pairing uses two `(G1, G2)` pairs. | +| `ACC_MSM_MIN_SCRATCH_BYTES` | `0x7000` | Historical accumulator MSM floor; preserves existing generated addresses for smaller circuits. | + +## Layout Construction + +`SolidityGenerator` chooses a stable `VK_MPTR` after proof-shape planning. This +matters because `outer-fewer-point-sets` can add dummy evals and point sets, +which changes the transcript-buffer bound. The verifier reserves: + +1. transient transcript buffer below `VK_MPTR`; +2. VK payload at `VK_MPTR`; +3. user challenge slots after the VK payload; +4. computed compatibility theta-relative slots; +5. decoded evals and decompressed commitments; +6. phase-scoped scratch regions; +7. one non-overlapping `trace_u256` log word after all live regions. + +`VerifierMemoryLayout::validate()` rejects: + +- unaligned starts or lengths; +- any generated region inside Solidity-reserved memory `[0x00..0x80)`; +- overlapping permanent regions; +- overlapping scratch regions that are live in the same `MemoryPhase`; +- PCS fixed-window overflows. + +Intentional reuse is represented by giving the same byte range disjoint +lifetimes. For example, `batch_invert_scratch`, quotient VM scratch, q_eval +source tables, q_com trace scratch, and final MSM scratch can share bytes when +their phases do not overlap. + +The `trace_u256` log word is deliberately not a historical fixed constant. +Trace hooks can run between reads from long-lived VK/eval/commitment memory, so +their one-word `mstore` must sit outside every permanent and phase-scoped +region. This avoids the old failure mode where a diagnostic log buffer inside a +large VK payload corrupted a later G1MSM input. + +The external quotient evaluator has a separate EVM memory space. Its trace hook +is logless because the evaluator is called through `STATICCALL`, so it uses the +callee-local quotient return buffer at `0x80` and overwrites that word with the +return-frame magic immediately before returning. It must not use the verifier's +high `trace_u256_mptr`, which can overlap the evaluator's VM stack in the +callee. + +## Theta-Relative Offsets + +`THETA_MPTR` is the anchor for the historical fixed verifier state. +`ThetaWindowLayout::compatibility()` computes the window starts from named slot +sizes, historical capacities, and padding, then validation pins the resulting +addresses. The offsets below are in 32-byte words from `THETA_MPTR`. + +| Offset | Region | Size | Justification | +| ---: | --- | ---: | --- | +| `0` | `theta` | 1 word | First challenge after user-phase challenges. | +| `1` | `beta` | 1 word | Permutation/lookup challenge. | +| `2` | `gamma` | 1 word | Permutation/lookup challenge. | +| `3` | `trash_challenge` | 1 word | Trashcan challenge. | +| `4` | `y` | 1 word | Identity-evaluation batching challenge. | +| `5` | `x` | 1 word | Evaluation point. | +| `6` | `x1` | 1 word | PCS multi-prepare commitment fold challenge. | +| `7` | `x2` | 1 word | PCS f_eval Horner challenge. | +| `8` | `x3` | 1 word | PCS interpolation point. | +| `9` | `x4` | 1 word | PCS final_com fold challenge. | +| `10` | `f_com` | 4 words | Trailing PCS commitment, padded G1. | +| `14` | `pi` | 4 words | Final KZG opening commitment, padded G1. | +| `18` | `acc_lhs` | 4 words | Public accumulator LHS point. | +| `22` | `acc_rhs` | 4 words | Public accumulator RHS point. | +| `26` | `x_n` | 1 word | Lagrange numerator. | +| `27` | `x_n_minus_1_inv` | 1 word | Inverse used by Lagrange terms. | +| `28` | `l_last` | 1 word | Last-row Lagrange value. | +| `29` | `l_blind` | 1 word | Blind-row Lagrange value. | +| `30` | `l_0` | 1 word | First-row Lagrange value. | +| `31` | `instance_eval` | 1 word | Locally computed public-input evaluation. | +| `32` | `quotient_eval` | 1 word | Expected opening scalar for the linearized commitment. | +| `33` | `quotient` scratch | 4 words | Production uses first two words for `x_split` and `1 - x_n`; trace treats four words as a G1-shaped point. | +| `37` | padding | 1 word | Kept unused to preserve old downstream offsets. | +| `38` | `f_eval` | 1 word | PCS aggregated evaluation. | +| `39` | `v` | 1 word | Final folded evaluation. | +| `40` | `final_com` | 4 words | Output of fused final PCS MSM. | +| `44` | `pairing_lhs` | 4 words | KZG pairing LHS G1. | +| `48` | `pairing_rhs` | 4 words | KZG pairing RHS G1. | +| `52` | `rot_points` | up to 28 words | Historical PCS fixed window for `x * omega^rot`. | +| `80` | `x1_powers` | up to 65 words | Historical PCS fixed window for powers of `x1`. | +| `145` | `q_com` / `q_eval_set` | 0 / up to 56 words | `q_com` is fused into MSM scratch, so it aliases `q_eval_set` with zero capacity. | +| `201` | `q_eval_cptr` | 1 word | Runtime calldata pointer to q_eval scalars. | +| `209` | `g1_identity` | 4 words | Zero-initialized padded G1 identity. | +| `220` | `reversed_evals` | `num_evals` words | Decoded scalar eval buffer. | +| `220 + num_evals` | commitment bases | dynamic | Decompressed proof G1s, contiguous by category. | + +The gaps between fixed windows are deliberate historical padding. Do not use +them as scratch without registering a `MemoryRegion` and a phase. + +## Dynamic Commitment Region + +The commitment region begins at: + +```text +comms_mptr_base = THETA_MPTR + (220 + num_evals) * WORD_BYTES +``` + +Each commitment is a padded G1 (`G1_BYTES == 0x80`). The category bases are: + +```text +advice +lookup multiplicity +permutation Z +lookup helpers +lookup accumulator Z +trashcan commitments +quotient limbs +``` + +`selector_acc_mptr` is the first word after all decompressed commitments. The +selector accumulator words are live during final linearization and final PCS +MSM, so the generic PCS scratch begins after that selector block: + +```text +quotient_tmp_mptr = selector_acc_mptr + num_simple_selectors * WORD_BYTES +PCS scratch base = quotient_tmp_mptr +``` + +## Scratch Lifetimes + +The planner validates by lifetime, not just by address. + +| Phase | Region examples | Notes | +| --- | --- | --- | +| `Transcript` | `[0, transcript_words * 0x20)` | Must stay below `VK_MPTR`. | +| `ScalarInv` | `VK_MPTR - 0x100` frame | Historical modexp scratch near the VK payload. | +| `LagrangeBatchInvert` | `batch_invert_scratch_mptr` | Reuses selector bytes before selector accumulators are live. | +| `QuotientVm` | quotient temps and stack | Used before PCS final MSM. | +| `PcsQEvalSourceTable` | rolled q_eval address table | Aliases `pcs_scratch_mptr`. | +| `PcsQComTrace` | optional q_com trace MSM | Aliases `pcs_scratch_mptr`; trace-only. | +| `PcsFinalMsm` | final MSM input and selector accumulators | Selector accumulators and final MSM must not overlap in this phase. | +| `AccumulatorMsm` | public accumulator MSM input | Length is derived from accumulator/VK shape. | +| `AccumulatorPairingBatch` | `[0x100, 0x320)` | Hash domain plus four G1 points for accumulator pairing batching. | +| `FinalPairing` | two-pair KZG pairing frame | Low-memory final precompile frame. | + +## Update Rules + +When changing generated memory usage: + +1. Add or update the named region in `VerifierMemoryLayout`. +2. Register it through `MemoryArena` with the narrowest correct lifetime. +3. Use named constants (`WORD_BYTES`, `G1_BYTES`, `G1_MSM_PAIR_BYTES`, etc.) + instead of raw byte literals when the literal describes layout. +4. If a fixed theta-relative offset changes, update `ThetaWindowLayout`, this + document, and the synthetic layout test. +5. If a scratch region can grow from circuit/VK shape, add it to + `PcsMemoryRequirements` or `VerifierMemoryLayoutConfig`. +6. Run `cargo test --lib --all-features` at minimum; for verifier-impacting + changes also run the ignored Poseidon/IVC commands from `TESTING.md`. diff --git a/proofs/solidity-verifier/docs/architecture/MIGRATION.md b/proofs/solidity-verifier/docs/architecture/MIGRATION.md new file mode 100644 index 000000000..5e314d953 --- /dev/null +++ b/proofs/solidity-verifier/docs/architecture/MIGRATION.md @@ -0,0 +1,518 @@ +# Historical migration: `halo2-solidity-verifier-exp` -> midnight-proofs + +Started: 2026-04-26. + +This document is retained as a historical porting log. It is not the current +implementation plan or public API reference; use `README.md` and +`docs/architecture/LOWERING_ARCHITECTURE_SPEC.md` for the maintained generator +architecture. + +## Goal + +Replace the vendored halo2 v0.4 dependency with a path dep on +`midnight-proofs` (the midfall fork) so the codegen-based Solidity +verifier in this crate verifies proofs produced by midnight-proofs + +midnight-curves on BLS12-381 / EIP-2537. + +The first verification target is the **poseidon** circuit fixture under +`midfall/proofs/solidity-verifier/fixtures/poseidon/` (`proof.bin`, +`vk.bin`, `instance.be`). + +## Why three crates change at once + +midnight-proofs differs from halo2 v0.4 in three ways that all affect +the Solidity verifier surface: + +1. **LogUp lookup argument** (`midnight_proofs::plonk::logup`) replaces + halo2's grand-product lookup. The proof carries one + *multiplicity* commitment + N *helper* commitments + one + *accumulator* commitment per lookup, plus eval at `x` and `omega*x`. +2. **Trash argument** (`midnight_proofs::plonk::trash`) is new. The + proof carries one trashcan commitment per `cs.trashcans()` entry and + one eval at `x`. +3. **KZG multi-prepare PCS** + (`midnight_proofs::poly::kzg::KZGCommitmentScheme::multi_open`) + replaces both halo2's GWC19 and SHPLONK emitters. The proof block + ends with `(x1, x2, f_com, x3, q_evals_per_set, x4, pi)` instead of + one `W` per rotation set. +4. **Keccak256 transcript** uses a domain separator + a 64-byte squeeze + (two-fork pattern) + `Fq::from_uniform_bytes` for challenge sampling. + This differs from halo2's "concat with 0x01 marker, single 32-byte + digest, mod-r reduce" recipe. + +## Plan + +``` +Step 1. Cargo.toml: dep swap (halo2_proofs/halo2_middleware/halo2_backend + + halo2curves) -> midnight-proofs path dep + midnight-curves + + ff + group. +Step 2. src/transcript.rs: full rewrite as Keccak256Transcript matching + midnight_proofs::CircuitTranscript byte-for-byte. + Includes round-trip test against the upstream Rust impl. +Step 3. src/lowering/encoding/mod.rs::ConstraintSystemMeta + Data: rebind to + midnight_proofs::plonk::ConstraintSystem; record logup + chunk counts, trashcan count, num_simple_selectors, + num_committed_instances. g1_to_u256s/g2_to_u256s migrated to + midnight-curves types via AsRef<[u8]> on FpRepr. + +------------------ Steps 1-3 land here. cargo check --lib + cargo + test --lib transcript pass. ------------------ + +Step 4. src/lowering/quotient_numerator/yul_emit.rs: implement permutation_computations, + lookup_computations, and trashcan_computations against the + midnight-proofs argument expressions + (proofs/src/plonk/{permutation,logup,trash}.rs::expressions). + The gate emitter is already ported. +Step 5. src/lowering/kzg/mod.rs: replace the rotation-set + emitter with the multi_prepare flow: + * read x1, x2 from squeeze + * read f_com (1 G1) + * read x3 from squeeze + * read q_evals (1 Fq per point set; point sets come from + construct_intermediate_sets after deduplication) + * read x4 from squeeze + * read pi (1 G1) + * compute the verifier MSM into PAIRING_LHS / PAIRING_RHS + Two G1s + #point-sets Fqs replace the previous trailing-W loop. +Step 6. templates/contracts/Halo2Verifier.sol: rewrite the Yul body to read the + new proof byte stream (compressed 48-byte G1 -> in-EVM + decompression -> EIP-2537 padded form), squeeze challenges via + the 64-byte two-fork keccak, and execute the PCS check from + Step 5. Embed the lookup helpers/accumulators + trashcans + between user-phase advices and the quotient limbs. +Step 7. templates/contracts/Halo2VerifyingKey.sol: regenerate the const layout to + match the new ConstraintSystemMeta (per-lookup chunk counts, + per-trashcan tables, num_simple_selectors). Constants block is + no longer fixed-size, so the embedded-VK / separate-VK paths + must agree on a length-prefix. +Step 8. examples/: port `compare_trace`, `trace`, `separately`, + `probe_revert`, `check_delta` to the new SolidityGenerator API. + Add a `verify_poseidon` example that loads + midfall/proofs/solidity-verifier/fixtures/poseidon/{proof.bin, + vk.bin, instance.be} and executes + encode_calldata + Evm::call against the rendered Halo2Verifier. +Step 9. tests/: PBT + soundness tests against the rendered verifier. + Mirror the existing approach in + midfall/proofs/solidity-verifier/tests/. +``` + +## What landed in Steps 1-7 + Step 8 scaffolding + +### Steps 1-3 + +* `Cargo.toml` — swapped dep tree; `cargo check --lib` resolves + midnight-proofs and midnight-curves. +* `src/transcript.rs` — new `Keccak256Transcript` matches + `CircuitTranscript` byte-for-byte. Three round-trip tests + pass: + * `empty_squeeze_matches_midnight_proofs` + * `common_scalar_then_squeeze_matches` + * `common_g1_then_squeeze_matches` +* `src/lowering/encoding/mod.rs::ConstraintSystemMeta` — walks + `midnight_proofs::plonk::ConstraintSystem`. New fields: + `lookup_chunks: Vec` (one per lookup), `num_lookups`, + `num_trashcans`, `num_simple_selectors`, `num_committed_instances`. + `num_advices()` and `num_challenges()` emit per-phase counts that + match the midnight-proofs proof byte stream. +* `src/lowering/encoding/mod.rs::Data` — gains `lookup_m_comms`, + `lookup_helper_comms: Vec>`, `lookup_z_comms`, + `trashcan_comms`, `lookup_evals: Vec<(m, [helpers], z, z_next)>`, + `trashcan_evals`. Permutation map is keyed by `Column`. +* `src/lowering/encoding/mod.rs::g1_to_u256s` / `g2_to_u256s` — now consume + midnight-curves `G1Affine` / `G2Affine`. The `FpRepr([u8; 48])` tuple + field is private upstream, so we read via `AsRef<[u8]>` and + `copy_from_slice`. +* `src/lowering/quotient_numerator/yul_emit.rs::Evaluator` — `gate_computations()` is fully + ported. `permutation_computations()`, `lookup_computations()`, and + `trashcan_computations()` are stubbed empty pending Step 4. + Expression walking uses the 10-callback `Expression::evaluate` + visitor; queries use `column_index()` / `rotation()` accessors + (private fields upstream). Selectors panic — they should already be + removed during `directly_convert_selectors_to_fixed`. +* `src/lowering.rs::SolidityGenerator` — took + `&ParamsKZG` and + `&VerifyingKey>`. The `params.g[0]` + field is crate-private upstream, so we use + `G1Affine::generator()` for the SRS G1 generator. `g2()` / `s_g2()` + return projective; we `.to_affine()` before EIP-2537 packing. + The then-current post-construction API exposed the + `nb_committed_instances` knob (defaulted to 0 for poseidon). +* `src/lowering/kzg/mod.rs` — stubbed empty. + Their previous halo2-era emitters did not map to multi-prepare. +* `src/lowering/render/models.rs::Halo2Verifier` — gains `num_lookups`, + `num_trashcans` fields so Step 6 templates can reference them. +* `src/lib.rs` — the test-only `__test_only_g1_to_u256s` now takes + `&midnight_curves::G1Affine`. Removed the `#![deny(missing_docs)]` + attribute since the full migration leaves several internal items + un-doc'd until Step 8. +* `src/evm.rs` — `encode_calldata` now uses `ff::PrimeField` directly. + +### Step 4 (2026-04-26) + +* `src/lowering/quotient_numerator/yul_emit.rs::Evaluator::permutation_computations()` — emits the + midnight-proofs permutation argument: l_0 boundary, l_last boundary, set-to-set + continuity, and per-set `(z_next * Π(eval + β·s + γ)) - (z_cur * Π(eval + δ_pow + γ))`. +* `src/lowering/quotient_numerator/yul_emit.rs::Evaluator::lookup_computations()` — full LogUp emitter: + `(l_0 + l_last) * Z` boundary, per-chunk + `h*Π(f + β) - Σ_j Π_{i≠j}(f_i + β)` helpers (computed via prefix·suffix), and the + accumulator `(z_next - z - sel·Σh)(t + β) + m` constraint. +* `src/lowering/quotient_numerator/yul_emit.rs::Evaluator::trashcan_computations()` — emits + `compressed - (1 - q)*trash` where `compressed` folds the trashcan's + constraint expressions with `trash_challenge`. +* `src/lowering/encoding/mod.rs::ConstraintSystemMeta` — gains `permutation_chunk_len` + and `simple_selector_cols: BTreeSet` (used by the permutation emitter + and the fixed-eval lookup). + +### Step 5 (2026-04-26) + +* `src/lowering/kzg/mod.rs` — module-level rewrite for the midnight-proofs + `KZGCommitmentScheme::multi_prepare` flow. +* `src/lowering/kzg/mod.rs::queries()` — builds the verifier query list + mirroring `verify_algebraic_constraints`: advice queries, permutation product + cur/next/last, lookup m/h/z/z_next, trashcan, fixed (non-simple), perm common, + linearization (= computed quotient). +* `src/lowering/kzg/mod.rs::construct_intermediate_sets_impl()` — codegen-time + port of the Rust algorithm. Buckets queries by `(commitment_id, point_set)` + and assigns `set_index`. `sort_sets()` then orders sets by ascending + cardinality (tiebreaker: original index). +* `src/lowering/kzg/mod.rs::computations()` — emits Yul for the multi-prepare + body in six blocks: + 1. Pre-compute `x * ω^rot` for every distinct rotation + 2. Pre-compute `x1` powers + 3. Per-set: `q_com[s] = Σ x1^i · c_i` and `q_eval_set[s] = Σ x1^i · evals_i` + 4. `f_eval` via Horner over reverse(point_sets) + Lagrange interpolation + 5. `final_com = msm_inner_product(q_coms ++ [f_com], powers(x4))`, + `v = inner_product(q_evals ++ [f_eval], powers(x4))` + 6. `PAIRING_LHS = Ï€`; `PAIRING_RHS = final_com - v·G + x3·π` +* `src/lowering/encoding/mod.rs::ConstraintSystemMeta::num_point_sets` + setter — populated + by `SolidityGenerator::generate_verifier` after building `Data` so that + `proof_len()` and `batch_open_extra_evals()` report the correct calldata size. +* `src/lowering.rs::generate_verifier()` — clones `meta` locally, populates + `num_point_sets` via the IntermediateSets simulation, and threads the + populated meta through evaluator + PCS emission + template fields. + +The emitted Yul references symbolic identifiers (`X1_MPTR`, `X2_MPTR`, +`X3_MPTR`, `X4_MPTR`, `F_COM_MPTR`, `PI_MPTR`, `Q_EVAL_CPTR`, `Q_COM_MPTR`, +`Q_EVAL_SET_MPTR`, `ROT_POINTS_MPTR`, `X1_POWERS_MPTR`, `F_EVAL_MPTR`, +`V_MPTR`, `FINAL_COM_MPTR`, `scalar_inv`) that Step 6 will define in the +template. + +Two tests pin the IntermediateSets builder (`intermediate_sets_*`); transcript +tests still pass. + +### Step 6 (2026-04-26) + +* `templates/contracts/Halo2Verifier.sol` — full Yul rewrite. The verifier now + consumes the midnight-proofs proof byte stream end-to-end: + 1. `transcript_init()` seeds a streaming Keccak256 buffer at memory + `[0x00..buf_len)` with the 31-byte `"Domain separator for transcript"`. + 2. `common_word(buf_len, w)` and `common_compressed_g1(buf_len, cptr)` + append `[PREFIX_COMMON=0x01, ...payload]` to the buffer. + 3. `squeeze_to(buf_len, mptr)` computes one Keccak digest of the + accumulated transcript bytes, reseeds the buffer with that digest, + and samples an Fq as `uint256(digest_be) mod r`. + 4. `decompress_g1(success, src, dst)` parses the zcash 48-byte + compressed encoding (top 3 flag bits + 381-bit x), runs + `modexp(x, 3, p)` then `modexp(y_sq, (p+1)/4, p)` to recover y, + and selects the correct sign by comparing `y` vs `p-y` limb-wise. + Identity points (infinity flag set) are written as four zero words. + 5. `scalar_inv(x)` computes `x^(r-2) mod r` via modexp. +* New MPTR layout in `templates/contracts/Halo2Verifier.sol`: + * `THETA, BETA, GAMMA, TRASH_CHALLENGE, Y, X` at `theta_mptr+0..+5` + * `X1, X2, X3, X4` at `theta_mptr+6..+9` + * `F_COM` (4 words) at `theta_mptr+10`, `PI` (4 words) at `+14` + * Lagrange / quotient / instance scratch shifted accordingly + * `F_EVAL`, `V`, `FINAL_COM` (4 words) added for the PCS pairing + reconstruction + * `ROT_POINTS`, `X1_POWERS`, `Q_COM`, `Q_EVAL_SET`, `Q_EVAL_CPTR_MPTR` + reserved for the Step 5 emitter +* New proof-read schedule in the Yul body (mirrors + `midnight_proofs::plonk::verifier::parse_trace` + + `verify_algebraic_constraints`): + * domain sep -> common(VK_DIGEST) -> common(num_instances) -> + common(each instance, byte-reversed) -> + * for each user phase: read `num_advices` compressed G1 + + squeeze `num_challenges` Fq (cumulative offset tracked via + `UserPhase::challenge_offset`) -> + * theta -> read multiplicities -> beta, gamma -> read perm Z -> + read lookup helpers + accumulators -> trash_challenge -> + read trashcans -> y -> read quotient limbs -> x -> + read evals -> x1, x2 -> read f_com (decompressed at F_COM_MPTR) -> + x3 -> read q_evals (Q_EVAL_CPTR saved for the PCS emitter) -> + x4 -> read pi (decompressed at PI_MPTR) +* The existing Lagrange / quotient / final-pairing blocks are + preserved unchanged (pure Fr arithmetic). +* `src/lowering/render/models.rs` — adds `UserPhase { num_advices, + num_challenges, challenge_offset }` plus six new fields on + `Halo2Verifier` (`user_phases`, `num_user_challenges`, + `num_lookups`, `num_permutation_zs`, `lookup_h_plus_acc`, + `num_trashcans`, `num_quotients`, `num_evals`, `num_point_sets`) + consumed by the new template. +* `src/lowering.rs::generate_verifier()` — populates the new fields + and threads the local `meta` clone (with `num_point_sets` baked in) + through the evaluator, PCS emitter, and template. +* `src/lowering.rs::static_working_memory_size()` — replaces the old + per-phase Keccak input estimate with a streaming-buffer estimate + based on total absorbed G1 + scalar bytes; the result is only a + lower bound on `vk_mptr`, the actual transcript scratch lives at + `[0x00..0x2200)` and the modexp scratch at `[0x2200..0x2400)`. + +The rendered Yul produces the entire end-to-end verifier flow but is +**not yet exercised** against a real proof — that requires the +Step 8 example driver. Existing unit tests (`cargo test --lib`) +continue to pass: 3 transcript tests + 2 IntermediateSets tests. + +### Step 7 (2026-04-26) + +* `templates/contracts/Halo2VerifyingKey.sol` — replaced the terse leading + comment with a full word-by-word layout map that documents every + named slot consumed by the Step 6 verifier: + + ``` + word 0 : vk_digest + word 1 : num_instances + word 2..10 : k, n_inv, omega, omega_inv, omega_inv_to_l, + has_accumulator, acc_offset, num_acc_limbs, + num_acc_limb_bits + word 11..14 : G1_BASE (4 words, EIP-2537 padded) + word 15..22 : G2_BASE (8 words, EIP-2537 padded) + word 23..30 : NEG_S_G2_BASE (8 words, EIP-2537 padded) + word 31.. : fixed_comms[i] (4 words each) + word ... : permutation_comms[i] (4 words each) + ``` + + The midnight-proofs migration deliberately bakes per-lookup chunk + counts, trashcan structure, and `num_simple_selectors` into the + Yul body (codegen-time constants), so the runtime VK layout stays + *exactly* the same shape as the BN254 / halo2 v0.4 era — only the + meaning of the words changed (Fq -> Fr scalars, EIP-2537 padded G1 + / G2 instead of 64-byte raw points, neg_s_g2 instead of pairing + precomputed factors). + +* `src/lowering/render/models.rs::tests` — new test module with two + asserts that pin the byte layout the verifier consumes: + * `vk_layout_byte_consistency`: synthesises a 31-scalar + + 2-fixed + 3-perm `Halo2VerifyingKey` and verifies + `len() == bytes().len() == 1632 (= 51 * 32)`. Spot-checks the + `vk_digest` head, `NEG_S_G2_BASE_MPTR` (word 23), + `fixed_comms[0]` (word 31), and `permutation_comms[0]` + (word 39) byte offsets. + * `vk_renders_and_returns_correct_length`: renders the VK + template and asserts the constructor emits + `return(0, 0x0660)` plus the expected `mstore(0x0000, ...)` + and `mstore(0x04e0, ...)` lines. + +* Manual `solc 0.8.30` compile of a representative VK render + (`solc --bin --optimize --via-ir --evm-version cancun`) succeeded + end-to-end, confirming the layout + trailing `return(0, len)` + produce a clean runtime bytecode (the 51-word data blob). + +The `cargo test --lib` suite now stands at 7/7 green: + * 3 transcript round-trip tests + * 2 IntermediateSets bucketing tests + * 2 VK layout / render tests + +### Step 8 scaffolding (2026-04-26, in-progress) + +* `Cargo.toml` — added `midnight-circuits` and `midnight-zk-stdlib` + as dev-deps (path = `../midfall/circuits` and + `../midfall/zk_stdlib`, both with `features = ["testing"]`), + plus `rand_chacha = "0.3.1"`. Added `[patch.crates-io]` and + `[patch."https://github.com/midnightntwrk/midnight-zk"]` redirects + for `midnight-proofs` / `midnight-curves` / `midnight-circuits`. + Without these patches `Hashable` etc. become ambiguous because + `keccak_sha3` (a transitive dep of `midnight-zk-stdlib`) pulls in + its own copy of `midnight-proofs`. Also enabled the + `circuit-params` feature on the main `midnight-proofs` dep so the + `Circuit` trait surface matches what `MidnightCircuit` expects. +* `src/lowering.rs::SolidityGenerator::new` — relaxed the + `num_instance_columns() <= 1` assertion to `<= 2`. ZkStdLib + always allocates two instance columns (one committed, one + non-committed), so the v0.4 tightness no longer applies. + At the time, callers selected the split through a post-construction + committed-instance setter. +* `src/lowering/kzg/mod.rs` — fixed two emitter bugs surfaced by + the first end-to-end render: + * Block 4 / Block 5 now bind `let Q_EVAL_CPTR := + mload(Q_EVAL_CPTR_MPTR)` at the top so the in-block + `calldataload(add(Q_EVAL_CPTR, ...))` references resolve. + * `scalar_inv(x, r)` calls collapsed to `scalar_inv(x)` to + match the template's helper signature (the helper bakes the + modulus internally). +* `templates/contracts/Halo2Verifier.sol` — wrapped the top-level body in + `assembly ("memory-safe") { ... }` to silence the legacy + stack-too-deep error path; with `--via-ir` solc 0.8.30 now + compiles the full ~117 kB output cleanly. +* `tests/poseidon_fixture.rs` — new integration test (gated behind + `feature = "evm"` and currently `#[ignore]`d, see below) that: + 1. Configures `SRS_DIR` to point at + `../midfall/zk_stdlib/examples/assets/bls_filecoin_2p6`. + 2. Builds the same `PoseidonExample` `Relation` the midfall + fixture was generated from (`std_lib.poseidon` over a + 3-element witness, hash exposed as the single public + input). + 3. Calls `setup_vk` / `setup_pk` / `prove::<_, Keccak256>` to + produce a fresh `(vk, proof, instance)` triple at `k = 6`. + 4. Sanity-checks via the native Rust verifier + (`midnight_zk_stdlib::verify::<_, Keccak256>`). + 5. Constructs a `SolidityGenerator` against the same VK with a config + that acknowledges the committed-instance column. + 6. Renders separate verifier/VK artifacts -> `compile_solidity` -> deploys + both contracts on Prague-spec revm (with EIP-2537 BLS12-381 + precompiles routed through `blst`). + 7. Encodes calldata through the generator's calldata helper and + calls the verifier; on `CallOutcome::Revert` it dumps the + full rendered Yul + proof + instance under + `target/poseidon-fixture-dump/` for post-mortem. + + **Status**: render + compile + deploy all succeed; the verifier + reverts mid-execution with empty payload at gas ~123 k against + the real proof. + + ### Root-cause analysis (2026-04-26) + + Bisecting the rendered Yul + dumped artefacts located the + failure: the PCS / quotient-fold sections read each G1 + commitment as four contiguous calldata words (the EIP-2537 + *padded* form, 128 bytes per point). The proof bytes that the + prover actually emits, however, are in zcash *compressed* form + (48 bytes per point). The two layouts disagree by a factor of + ~2.7×, so: + + * `Data::new` (in `src/lowering/encoding/mod.rs`) advances its calldata + cursors with a 4-word stride (`+ 4 * count`, i.e. 128 bytes + per G1) instead of the correct 48-byte stride. + * The PCS emitter (`src/lowering/kzg/mod.rs`) loads each + commitment with `c.comm.words()`, which expands to four + `calldataload(...)` calls at consecutive 32-byte offsets. + Against compressed proof bytes this returns garbage (or the + next commitment's bytes, or zeros past `calldatasize()`). + * The quotient-fold block in + `templates/contracts/Halo2Verifier.sol::~line 855` reads + `LAST_QUOTIENT_X_CPTR..LAST_QUOTIENT_X_CPTR+0x80` from + calldata for each quotient limb — same false stride. + * The `eval_cptr` derived from `quotient_comm_start + 4 * + num_quotients` lands ~1.6 kB past where evals actually live + (since the codegen still over-counts the G1 region by + ~2.7×), so the renderer emits eval reads at offsets like + `calldataload(0x0a64)` which is past the end of the actual + eval block. + + The proof BYTE LAYOUT itself is internally consistent + (`proof_len` already uses the correct `0x30` stride; the + prover and `verifyProof`'s `eq(proof_len, calldataload(...))` + check pass) — only the codegen's *interpretation* of those + bytes is wrong. + + ### Fix plan + + The cleanest fix is to decompress every commitment during + proof reading and have the PCS / quotient-fold work from the + decompressed memory copy: + + 1. Allocate a contiguous decompressed-commitments memory + region in the verifier's static memory map. Suggested + offsets, anchored after the existing `Q_EVAL_CPTR_MPTR` + (`theta_mptr + 200`): + + ADVICE_COMMS_MPTR_BASE = theta_mptr + 220 // 4*num_advices words + LOOKUP_M_COMMS_MPTR_BASE = + 4*num_advices // 4*num_lookups words + PERM_Z_COMMS_MPTR_BASE = + 4*num_lookups + LOOKUP_HELPER_COMMS_MPTR_BASE = + 4*num_perm_zs + LOOKUP_Z_COMMS_MPTR_BASE = + 4*helpers_total + TRASHCAN_COMMS_MPTR_BASE = + 4*num_lookups + QUOTIENT_LIMB_COMMS_MPTR_BASE = + 4*num_trashcans + + 2. In `templates/contracts/Halo2Verifier.sol`, after each + `common_compressed_g1(buf_len, proof_cptr)` call inside + the user-phase / lookup / perm / quotient loops, call + `decompress_g1(success, proof_cptr, + )` and persist the + decompressed point at the chosen MPTR. (The pattern + already exists for `f_com` and `pi`.) + + 3. In `src/lowering/encoding/mod.rs::Data::new`, replace the + calldata-anchored EcPoints with memory-anchored ones: + + let advice_comms = (0..meta.advice_indices.len()) + .map(|i| EcPoint::new(Ptr::memory("ADVICE_COMMS_MPTR_BASE") + 4 * i)) + .collect(); + // ...same for lookup_m, perm_z, helpers, lookup_z, trashcan, + // quotient_limbs. + + With memory-anchored pointers, `c.comm.words()` already + emits `mload(...)` (see `Word::Display`), so the PCS code + compiles correctly without any changes there. + + 4. Recompute `eval_cptr` from the correct compressed-G1 + stride. The byte offset relative to `proof_cptr` is + `0x30 * (advices + lookups + perm_zs + helpers_total + + lookups + trashes + quotients)`. Because `Ptr::add(usize)` + is word-aligned, build the eval cursor via + `Ptr::calldata(proof_cptr.value().as_usize() + 0x30 * + pre_eval_g1_count)`. + + 5. Replace the calldata-anchored quotient fold in + `templates/contracts/Halo2Verifier.sol` (~line 855) with a memory + loop over `QUOTIENT_LIMB_COMMS_MPTR_BASE`: + + let cptr := add(QUOTIENT_LIMB_COMMS_MPTR_BASE, + mul(0x80, sub(num_quotients, 1))) + // load (x_hi, x_lo, y_hi, y_lo) at cptr, then fold via + // ec_mul_acc(x_n) + ec_add_acc. + + 6. Drop the `FIRST_QUOTIENT_X_CPTR` / `LAST_QUOTIENT_X_CPTR` + constants from the template; they no longer have meaning + once the quotient limbs live in memory. + + Once those changes land: + + * `tests/poseidon_fixture.rs` runs end-to-end (drop + `#[ignore]`). + * `cargo test --features evm` should report two passing + tests: the lib suite + the Step 8 fixture. + + No changes are needed in + `src/lowering/kzg/mod.rs::commitment_map` or the PCS Yul + emission; the abstraction over `EcPoint` already works. + +## Pending work (Step 8 follow-up + Step 9) + +| Step | Files | Notes | +|------|-------|-------| +| 8 (cont.) | `templates/contracts/Halo2Verifier.sol`, `src/lowering/**` | chase the rendered-Yul revert against the midfall poseidon fixture's `verifier_trace.bin` and `rust_trace.json`. Once both traces agree element-by-element, drop the `#[ignore]` on `tests/poseidon_fixture.rs` and assert the verifier returns 1. Optionally port `examples/separately.rs`, `examples/trace.rs`, and `examples/compare_trace.rs` to the midnight-proofs API (currently they reference v0.4 types and don't compile). | +| 9 | `tests/` | PBTs + soundness flips. Mirror the existing approach in `midfall/proofs/solidity-verifier/tests/`. | + +## How to validate the current state + +```sh +$ cargo check --lib + Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.08s + +$ cargo test --lib --features evm +running 7 tests +test lowering::pcs::tests::intermediate_sets_dedups_commitments ... ok +test lowering::pcs::tests::intermediate_sets_partitions_by_rotation_set ... ok +test lowering::template::tests::vk_layout_byte_consistency ... ok +test lowering::template::tests::vk_renders_and_returns_correct_length ... ok +test transcript::tests::common_g1_then_squeeze_matches ... ok +test transcript::tests::common_scalar_then_squeeze_matches ... ok +test transcript::tests::empty_squeeze_matches_midnight_proofs ... ok +test result: ok. 7 passed; 0 failed; ... + +$ cargo test --features evm --test poseidon_fixture +running 1 test +test poseidon_renders_compiles_and_verifies ... ignored +test result: ok. 0 passed; 0 failed; 1 ignored; ... + +$ cargo test --features evm --test poseidon_fixture -- --ignored --nocapture +# (currently fails with "verifier reverted with gas_used = ~123410"; +# see Step 8 follow-up notes for the in-flight debugging plan) +``` + +`examples/`, `src/test.rs`, and the rendered Yul are *not yet* fully +exercised end-to-end and will fail to verify until Steps 7-9 are +completed (in particular, Step 7 finalises the Halo2VerifyingKey +template and Step 8 wires up an example driver that compiles the +rendered Yul under solc and executes it against the poseidon +fixture). diff --git a/proofs/solidity-verifier/docs/audit/AUDIT.md b/proofs/solidity-verifier/docs/audit/AUDIT.md new file mode 100644 index 000000000..b85c509aa --- /dev/null +++ b/proofs/solidity-verifier/docs/audit/AUDIT.md @@ -0,0 +1,2600 @@ +# CTF Vulnerability Analysis: Halo2 Solidity Verifier + +## 2026-05-02 audit addendum: PCS scratch layout and instance-column shape + +Scope: current dirty worktree for the Halo2/Midnight Solidity verifier +generator, especially: + +- `templates/contracts/Halo2Verifier.sol` +- `src/codegen.rs` +- `src/codegen/evaluator.rs` +- `src/codegen/pcs.rs` + +This pass focuses on generated verifier soundness, transcript equivalence with +native Midnight verification, memory layout safety, and trace-mode behavior. + +### Findings overview + +| ID | Severity | Title | +| --- | --- | --- | +| D-1 | High | Variable PCS scratch writes can overwrite live verifier state | +| D-2 | Medium | Committed-instance transcript shape is hardcoded to one identity | +| D-3 | Medium | Multiple public instance columns are accepted but aliased | +| D-4 | Low | Trace-only precompile calls perturb verifier success | + +### D-1. Variable PCS scratch writes can overwrite live verifier state + +Severity: High. + +The template reserves fixed scratch windows for rotations, x1 powers, q_com, +and q_eval_set: + +- `templates/contracts/Halo2Verifier.sol`: `ROT_POINTS_MPTR` +- `templates/contracts/Halo2Verifier.sol`: `X1_POWERS_MPTR` +- `templates/contracts/Halo2Verifier.sol`: `Q_COM_MPTR` +- `templates/contracts/Halo2Verifier.sol`: `Q_EVAL_SET_MPTR` +- `templates/contracts/Halo2Verifier.sol`: `Q_EVAL_CPTR_MPTR` +- `templates/contracts/Halo2Verifier.sol`: `G1_IDENTITY_MPTR` + +The PCS emitter then writes circuit-dependent lengths into those fixed windows: + +- `src/codegen/pcs.rs`: `distinct_rotations` +- `src/codegen/pcs.rs`: `nb_x1_powers` +- `src/codegen/pcs.rs`: q_eval_set persistence under + `Q_EVAL_SET_MPTR` + +There is no capacity check in `Halo2Verifier::validate_layout`; it validates +proof, VK, commitment, and selector layout, but not the PCS scratch bounds. + +Impact: + +- A circuit with enough distinct rotations, enough commitments in one point + set, or wide enough point sets can overwrite adjacent live state. +- Overwrites can corrupt `Q_EVAL_CPTR_MPTR`, `G1_IDENTITY_MPTR`, decoded evals, + or copied commitments. +- The transcript can absorb one set of q_eval scalars while the PCS algebra + reads another memory location, making the generated verifier check a + corrupted statement. + +Recommendation: + +- Derive the scratch offsets and reserved sizes from the generated circuit + dimensions. +- Or reject unsupported dimensions in the generator before rendering Solidity. +- Add layout validation for: + - `distinct_rotations.len()` + - `nb_x1_powers` + - total q_eval_set width, i.e. `sum(point_sets[s].len())` + - any trace-only MSM scratch reuse + +### D-2. Committed-instance transcript shape is hardcoded to one identity + +Severity: Medium. + +The Solidity verifier always absorbs one committed public-instance commitment: + +```yul +// Absorb committed_pi = G1Affine::identity() +mstore(buf_len, 0) +mstore(add(buf_len, 0x20), 0) +mstore(add(buf_len, 0x40), 0) +mstore(add(buf_len, 0x60), 0) +buf_len := add(buf_len, 0x80) +``` + +Relevant code: + +- `templates/contracts/Halo2Verifier.sol`: unconditional committed identity absorb +- `src/generator/api.rs`: `GeneratorConfig` carries + `num_committed_instances` +- `src/generator/api.rs`: `SolidityGenerator::try_new` validates supported + committed-instance shapes + +Native Midnight verification absorbs exactly the committed instance commitments +provided to the verifier. Zero committed instance columns means absorb none; +two committed columns means absorb two. The Solidity template currently hashes +exactly one identity regardless of the generator setting. + +Impact: + +- With the default generator setting, the Solidity verifier transcript diverges + from native verification by one identity commitment. +- With more than one committed instance column, the Solidity verifier omits the + remaining commitments. +- Integrators can generate a verifier for a different Fiat-Shamir transcript + than the intended circuit/proof statement. + +Recommendation: + +- Enforce exactly one committed identity column if that is the only supported + Midnight deployment shape. +- Otherwise generate transcript logic from `num_committed_instances`. +- If commitments are not always identity, expose and validate them explicitly in + the verifier ABI. + +### D-3. Multiple public instance columns are accepted but aliased + +Severity: Medium. + +The generator accepts up to two instance columns: + +```rust +if vk.cs().num_instance_columns() > 2 { + return Err(GeneratorError::TooManyInstanceColumns { ... }); +} +``` + +But all non-committed instance queries resolve to one flat `INSTANCE_EVAL_MPTR` +value: + +- `templates/contracts/Halo2Verifier.sol`: computes one `instance_eval` from one flat + `instances` array +- `src/codegen.rs`: non-committed instance queries return + `self.data.instance_eval` +- `src/codegen/evaluator.rs`: `instance_eval_at` returns + `self.data.instance_eval` for all non-committed columns + +Native Midnight verification absorbs and evaluates each non-committed instance +column separately. + +Impact: + +- A circuit with two normal public instance columns can be generated without an + error. +- The Solidity verifier treats constraints over column 1 as constraints over + the same flat public-input evaluation used for column 0. +- Proofs are checked against an aliased public-input statement rather than the + native per-column statement. + +Recommendation: + +- Reject configurations with more than one non-committed instance column. +- Or generate a column-separated ABI, transcript absorb, and Lagrange + evaluation path. +- Add tests covering at least: + - zero committed plus two non-committed instance columns + - one committed plus one non-committed instance column + - two committed instance columns + +### D-4. Trace-only precompile calls perturb verifier success + +Severity: Low. + +The new trace materialization paths use: + +```yul +success := and(success, staticcall(...)) +``` + +Yul evaluates the `staticcall` even when `success` is already false. The call +result is then folded into production `success`. + +Relevant code: + +- `src/codegen/pcs.rs`: trace per-set q_com materialization +- `templates/contracts/Halo2Verifier.sol`: trace linearization commitment + materialization + +Impact: + +- Trace builds can enter EC precompiles on paths that production logic intends + to skip after a prior failure. +- Trace-only precompile failures can change the verifier result. +- Trace output can stop being a faithful diagnostic view of the production + verifier. + +Recommendation: + +- Guard diagnostic precompile calls with `if success { ... }`. +- Or accumulate trace precompile status in a separate `trace_success` variable + that does not affect verifier acceptance. +- Keep trace scratch isolated from production scratch regions. + +Test status: + +- `cargo test --lib --quiet` failed with 64 passing tests and one failure: + `codegen::tests::failed_success_paths_do_not_enter_ec_precompiles`. +- The failing test confirms that `and(success, staticcall(...))` reappeared in + trace paths. + +--- + +## 2026-05-01 audit pass: current split BLS12-381 verifier generator + +Scope: current Halo2/Midnight Solidity verifier generator, especially: + +- `templates/contracts/Halo2Verifier.sol` +- `templates/contracts/Halo2QuotientEvaluator.sol` +- `src/codegen.rs` +- `src/codegen/pcs.rs` +- `src/transcript.rs` + +This pass focuses on verifier soundness, fail-closed behavior, transcript +equivalence, deployment binding, and proof-input canonicality. + +### Findings overview + +| ID | Severity | Title | +| --- | --- | --- | +| C-1 | High | Verifier can be deployed with an arbitrary quotient evaluator | +| C-2 | High | Missing precompile-existence and return-size checks can fail open on wrong chains | +| C-3 | Medium | Not all proof G1 commitments are validated at parse time | +| C-4 | Medium | Rust transcript reader/writer are asymmetric for G1 | +| C-5 | Low/Medium | Denominator-zero cases silently compute inverse as zero | +| C-6 | Informational | Add an end-of-proof cursor check | + +### C-1. Verifier can be deployed with an arbitrary quotient evaluator + +Severity: High. + +The verifier pins the VK by expected length and codehash, but the external +quotient evaluator is only checked as non-empty code at construction time. The +constructor then stores whatever `authorizedQuotient.codehash` was passed in and +trusts that contract forever. + +Relevant code: + +- `templates/contracts/Halo2Verifier.sol`: `AUTHORIZED_QUOTIENT` +- `templates/contracts/Halo2Verifier.sol`: quotient constructor branches +- `templates/contracts/Halo2Verifier.sol`: external quotient call in the batched identity + numerator reconstruction block +- `templates/contracts/Halo2QuotientEvaluator.sol`: expected fallback return frame + +The quotient evaluator returns: + +```text +word 0: magic/version +word 1: linearization expected eval +word 2..: simple-selector accumulators +``` + +The verifier checks only the magic word and then stores the returned +linearization scalar and selector accumulators: + +```yul +if iszero(staticcall(... quotientEvaluator ...)) { revert(0, 0) } +if iszero(eq(mload(q_out), QUOTIENT_MAGIC)) { revert(0, 0) } +mstore(QUOTIENT_EVAL_MPTR, mload(add(q_out, 0x20))) +``` + +Impact: + +- A malicious deployment can wire a quotient evaluator that returns + attacker-chosen linearization values. +- That effectively removes or rewrites the algebraic constraint check enforced + by the quotient numerator block. +- This is not caller-exploitable after an honest deployment, but it is a serious + deployment-integrity footgun for factories, registries, copied deployment + scripts, and third-party verifier addresses. + +Recommendation: + +- Generate and hard-code `EXPECTED_QUOTIENT_CODEHASH`, mirroring + `EXPECTED_VK_CODEHASH`. +- Optionally also hard-code `EXPECTED_QUOTIENT_LENGTH`. +- Include both VK and quotient evaluator hashes in deployment artifacts. +- Add a deployment test that rejects a quotient evaluator with the same ABI but + different runtime bytecode. + +### C-2. Missing precompile-existence and return-size checks can fail open on wrong chains + +Severity: High. + +The verifier assumes the EIP-2537 BLS12-381 precompiles exist at: + +```text +0x0b BLS12_G1ADD +0x0c BLS12_G1MSM +0x0f BLS12_PAIRING_CHECK +``` + +Most helper paths check only the `staticcall` success bit. A call to a +non-existent address can succeed with empty returndata and leave the output +memory unchanged. The code then consumes whatever was already in that memory +slot. + +Relevant code: + +- `templates/contracts/Halo2Verifier.sol`: `ec_add_acc` +- `templates/contracts/Halo2Verifier.sol`: `ec_mul_acc` +- `templates/contracts/Halo2Verifier.sol`: `ec_add_tmp` +- `templates/contracts/Halo2Verifier.sol`: `ec_mul_tmp` +- `templates/contracts/Halo2Verifier.sol`: `ec_pairing` +- `src/codegen/pcs.rs`: generated final MSM and pairing-input calls + +Impact: + +- On chains or local forks without EIP-2537 at those exact addresses, invalid + proofs may be accepted. +- This is catastrophic for wrong-chain deployment and dangerous for test + environments that do not faithfully model the precompiles. + +Recommendation: + +- After every precompile call, require the exact expected `returndatasize()`: + - `0x80` for G1 point outputs, + - `0x20` for pairing/modexp scalar outputs. +- Add a constructor or first-call self-test for G1ADD, G1MSM, and PAIRING. +- Fail closed if any precompile behavior differs from the expected EIP-2537 + semantics. + +### C-3. Not all proof G1 commitments are validated at parse time + +Severity: Medium. + +Proof G1 commitments are absorbed into the transcript and copied from calldata +directly. They are validated only if they later participate in an EIP-2537 +precompile call. + +Relevant code: + +- `templates/contracts/Halo2Verifier.sol`: `common_uncompressed_g1` +- `templates/contracts/Halo2Verifier.sol`: proof commitment read loops +- `src/codegen/pcs.rs`: query construction and final MSM staging + +The query builder includes advice commitments only when they appear in the +verifier query list. A circuit with a committed column that is transcript- +absorbed but never queried could allow invalid G1 bytes to influence the +Fiat-Shamir transcript without ever being rejected by a precompile. + +Impact: + +- Generated verifiers can accept transcripts the native verifier would reject. +- For dead commitments this is mostly a challenge-grinding or verifier- + equivalence issue, but the invariant is fragile as circuits and query sets + change. + +Recommendation: + +- Validate every proof G1 as it is read, or prove/enforce at codegen time that + every absorbed G1 is later validated by a precompile. +- Reject non-zero EIP-2537 padding bytes rather than only masking them before + hashing. +- Add negative tests with unused malformed commitments for circuits that contain + unqueried committed columns. + +### C-4. Rust transcript reader/writer are asymmetric for G1 + +Severity: Medium. + +The Rust helper transcript writes G1 points by absorbing the EIP-2537 padded +128-byte uncompressed form: + +```rust +pub fn write_g1(&mut self, point: &G1Projective) -> io::Result<()> { + self.common_g1(point)?; + let repr = point.to_bytes(); + self.stream.write_all(repr.as_ref()) +} +``` + +But `read_g1` absorbs the raw compressed bytes before decompressing: + +```rust +self.absorb_bytes(bytes.as_ref()); +G1Projective::from_bytes(&bytes) +``` + +Relevant code: + +- `src/transcript.rs`: `common_g1` +- `src/transcript.rs`: `read_g1` +- `src/transcript.rs`: `write_g1` + +Impact: + +- Rust-side proof generation and Rust-side verification can diverge when this + transcript is used for both. +- Trace comparison or fixture tooling may certify a different Fiat-Shamir + transcript than the Solidity verifier. +- This directly contradicts the file-level transcript spec, which says G1 + input is the 128-byte EIP-2537 padded uncompressed form. + +Recommendation: + +- In `read_g1`, read and decompress the compressed point first, then call + `common_g1(&point)`. +- Add a round-trip test that `write_g1` and `read_g1` produce the same + transcript state for a non-identity point and for the identity point. + +### C-5. Denominator-zero cases silently compute inverse as zero + +Severity: Low/Medium. + +Several verifier paths call `scalar_inv` on values that are expected to be +non-zero by Fiat-Shamir. If the denominator is zero, the modexp-based inverse +returns zero and the verifier continues with bogus arithmetic. + +Relevant code: + +- `templates/contracts/Halo2Verifier.sol`: `scalar_inv` +- `templates/contracts/Halo2Verifier.sol`: Lagrange batch inversion around + `x_n_minus_1` +- `src/codegen/pcs.rs`: PCS interpolation at `x3` + +Examples: + +- `x^n - 1 = 0` in the Lagrange/instance evaluation block. +- `x3 = rotation_point` in PCS interpolation. + +Impact: + +- The event is Fiat-Shamir-negligible under the configured challenge sampling, + but continuing after a zero denominator makes the soundness error implicit + and harder to reason about. + +Recommendation: + +- Explicitly reject zero denominators before inversion. +- Treat this as defensive hardening with tiny gas cost compared to the proof. + +### C-6. Add an end-of-proof cursor check + +Severity: Informational. + +The ABI length checks make this likely redundant today, but the verifier should +assert the transcript parser consumed exactly the expected proof bytes. + +Relevant code: + +- `templates/contracts/Halo2Verifier.sol`: proof length check +- `templates/contracts/Halo2Verifier.sol`: transcript proof cursor after `pi` + +Recommendation: + +- After reading `pi`, assert: + +```yul +success := and(success, eq(proof_cptr, NUM_INSTANCE_CPTR)) +``` + +This catches future proof-layout drift cheaply and makes the raw calldata parser +less brittle. + +### Recommended fix priority + +1. Pin the quotient evaluator by generated codehash and length. +2. Add `returndatasize()` checks and precompile self-tests. +3. Fix `read_g1` transcript absorption to match `write_g1` and Solidity. +4. Validate every absorbed proof G1 or enforce that every absorbed G1 is later + precompile-validated. +5. Reject zero inversion denominators. +6. Add the end-of-proof cursor check. + +--- + +## 2026-04-30 audit addendum: current Halo2 BLS12-381 Solidity/Yul verifier + +Scope: `templates/contracts/Halo2Verifier.sol`, `src/codegen.rs`, and the compact quotient +interpreter generated for the current IVC verifier branch. + +This pass focuses on verifier soundness, fail-closed behavior, proof/call-data +canonicality, and deployment risks in the generated Solidity/Yul verifier. + +### Findings overview + +| ID | Severity | Title | +| --- | --- | --- | +| A-1 | Medium/High | EIP-2537 precompile calls can fail open on unsupported or mismatched chains | +| A-2 | Medium | Non-canonical G1 encodings are transcript-normalized instead of rejected | +| A-3 | Low/Medium | `verifyProof(bytes,uint256[])` hand-parses calldata but does not enforce canonical ABI offsets | +| A-4 | Low | Compact quotient VM is correctness-critical and needs differential tests | +| A-5 | Informational | Current verifier remains over the 24KB EIP-170 runtime limit | + +### A-1. EIP-2537 precompile calls can fail open on unsupported or mismatched chains + +Severity: Medium/High, depending on deployment target. + +The verifier assumes the BLS12-381 precompiles at `0x0b`, `0x0c`, and `0x0f` +exist and implement the expected EIP-2537 semantics. The helper functions only +use the `staticcall` success bit, and `ec_pairing` additionally reads +`mload(scratch)` without checking that the call returned exactly one word. + +Relevant code: + +- `templates/contracts/Halo2Verifier.sol`: `ec_add_acc`, `ec_mul_acc`, `ec_add_tmp`, + `ec_mul_tmp` +- `templates/contracts/Halo2Verifier.sol`: `ec_pairing` +- `templates/contracts/Halo2Verifier.sol`: public accumulator MSM calls +- `templates/contracts/Halo2Verifier.sol`: final proof pairing check + +On a chain where those precompiles are absent or incompatible, a call to an +empty account can return success with zero return data. In that case output +memory may contain stale values. The pairing helper is especially sensitive: + +```yul +ret := staticcall(gas(), 0x0f, pairing_input_mptr, 0x240, scratch, 0x20) +ret := and(ret, mload(scratch)) +``` + +If the precompile is absent, `ret` may be true and `mload(scratch)` is not a +trusted pairing result. + +Impact: + +- Invalid proofs may be accepted on an unsupported or mismatched EVM fork. +- The verifier does not fail closed when its required cryptographic precompiles + are unavailable. + +Recommendation: + +- After every precompile call, require the exact expected `returndatasize()`. +- Require `0x80` bytes for G1 add/MSM-style point outputs. +- Require `0x20` bytes for pairing and scalar inversion/modexp-style outputs. +- Add a constructor self-test that rejects deployment if the target chain does + not implement the expected BLS12-381 precompile semantics. + +### A-2. Non-canonical G1 encodings are transcript-normalized instead of rejected + +Severity: Medium. + +`common_uncompressed_g1` copies 128 bytes from calldata, masks the high 16 bytes +of `x_hi` and `y_hi` before transcript absorption, but does not reject nonzero +padding. + +Relevant code: + +- `templates/contracts/Halo2Verifier.sol`: `common_uncompressed_g1` +- `templates/contracts/Halo2Verifier.sol`: proof commitment reads that call + `read_g1_point` + +This creates proof malleability: multiple calldata encodings can hash to the +same transcript point. The current design is only safe if every absorbed point +is later validated by an EIP-2537 precompile in a way that rejects the original +non-canonical bytes. That invariant is fragile for generated verifiers because a +commitment can become unused, zero-weighted, or protocol-dependent. + +Impact: + +- Non-canonical proofs can be accepted. +- Callers that commit to calldata or proof bytes may observe multiple encodings + of the same transcript. +- Future circuits or codegen changes can accidentally absorb a malformed point + that is never validated by a precompile. + +Recommendation: + +- Reject nonzero padding in `x_hi` and `y_hi` at transcript read time. +- Reject coordinates outside the BLS12-381 base field before absorption. +- Prefer validating every absorbed G1 point independently of whether it later + appears in an MSM. + +### A-3. `verifyProof(bytes,uint256[])` hand-parses calldata but does not enforce canonical ABI offsets + +Severity: Low/Medium. + +The public Solidity signature uses dynamic ABI arguments: + +```solidity +function verifyProof(bytes calldata proof, uint256[] calldata instances) + public + view + returns (bool) +``` + +The assembly body ignores Solidity's decoded `proof` and `instances` offsets +and instead reads fixed calldata locations such as `PROOF_LEN_CPTR`, +`PROOF_CPTR`, and `INSTANCE_CPTR`. The verifier checks the assumed proof length, +instance count, and total `calldatasize()`, but does not check that the ABI head +offsets actually point to those locations. + +Relevant code: + +- `templates/contracts/Halo2Verifier.sol`: `verifyProof` signature +- `templates/contracts/Halo2Verifier.sol`: proof length, instance count, and calldata size + checks + +This is probably not a direct soundness break because the verifier consistently +uses its raw calldata layout. It is still calldata malleability and can surprise +wrappers, calldata hash commitments, relayers, and off-chain tooling that expect +canonical ABI encoding. + +Recommendation: + +- Either use `proof.offset`, `proof.length`, `instances.offset`, and + `instances.length` directly in assembly. +- Or explicitly assert that the ABI head offsets match the expected canonical + layout before parsing proof bytes. + +### A-4. Compact quotient VM is correctness-critical and needs differential tests + +Severity: Low. + +The compact quotient identity interpreter introduces a generated bytecode +program plus a Yul VM. The program is not user-controlled, so this is not an +injection issue. However, a compiler or optimizer bug in this layer can silently +change the quotient identity that the verifier checks. + +Relevant code: + +- `src/codegen.rs`: compact quotient opcode definitions +- `src/codegen.rs`: identity expression parser and program builder +- `templates/contracts/Halo2Verifier.sol`: quotient VM dispatch loop + +Recommended tests: + +- Render both the old straight-line quotient evaluator and the compact VM for + the same circuit, then assert identical quotient values. +- Fuzz identity expression trees against the VM lowering. +- Include gates, simple selectors, lookups, permutations, constants, rotations, + trash challenges, and zero-term edge cases. +- Keep a debug render mode that can emit both implementations for differential + testing. + +### A-5. Current verifier remains over the 24KB EIP-170 runtime limit + +Severity: Informational. + +The latest IVC architecture notes report: + +- verifier runtime: approximately 25,598 bytes +- EIP-170 limit: 24,576 bytes + +Relevant code/documentation: + +- `ARCHITECTURE.md`: compact quotient VM results and remaining size budget + +This is not a proof-soundness issue, but it is a deployment blocker on +mainnet-like chains that enforce EIP-170 unless the verifier is split further or +the target chain disables/raises the contract size limit. + +### Open questions + +- Is the deployment target guaranteed to support the exact EIP-2537 precompile + addresses and return semantics used by this verifier? +- Is calldata uniqueness important for the consuming protocol? +- Are proof bytes or calldata hashes committed elsewhere by relayers, bridges, + settlement contracts, or off-chain indexers? +- Should the generated verifier be treated as a generic Halo2 verifier, or as a + Midnight/Midfall-specific verifier with a fixed transcript, accumulator + encoding, and proof layout? + +--- + +I analyzed both the current template (`templates/contracts/Halo2Verifier.sol`) and the legacy artifact (`generated/Halo2Verifier.sol`). Here are the findings, ranked by exploitability. + +--- + +## 1. CRITICAL — Caller-controlled VK in `generated/Halo2Verifier.sol` (legacy artifact) + +The committed pre-fix verifier in `generated/` still has the original signature: + +```solidity +function verifyProof(address vk, bytes calldata proof, uint256[] calldata instances) public view returns (bool) { + ... + extcodecopy(vk, VK_MPTR, 0x00, 0x40) // copies first 64 bytes of *attacker* contract + ... + extcodecopy(vk, VK_MPTR, 0x00, 0x04a0) // copies the entire VK area from *attacker* contract +} +``` + +Since `vk` is a function argument with **no on-chain validation**, an attacker deploys a malicious VK contract whose runtime bytecode is the raw 32-byte words returned via `RETURN(0,0x4a0)`, then calls `verifyProof(maliciousVk, junkProof, junkInstances)` and is accepted. + +Concrete forging recipe: +- Set `g2 = -neg_s_g2` (i.e. supply the *same* G2 point at both `G2_*_MPTR` and `NEG_S_G2_*_MPTR`). The pairing equation `e(LHS, g2) · e(RHS, neg_s_g2) = 1` collapses to `e(LHS+RHS, g2) = 1`, which holds for **any** `LHS = -RHS`. +- Or set both G2 points to the identity-image and skip subgroup checks entirely (verifier never validates G2). +- Choose `num_instances = 0`, `vk_digest = 0`, `omega = 1`, `n_inv = 1`, `g1 = O` to make every check pass. + +The new template fixes this with `AUTHORIZED_VK` + `EXPECTED_VK_CODEHASH` (commit `54b2943` "pin one authorized VK at deployment"), but anyone deploying the file under `generated/` is still vulnerable. + +--- + +## 2. HIGH — No subgroup / G2 check on VK-supplied curve points + +`read_ec_point` only enforces `x,y ∈ Fq` and `y² = x³ + 3` for **G1** points read from calldata. For BN254 G1 the prime-order group equals the curve, so on-curve ⇒ in-subgroup. **G2 points (`g2_*`, `neg_s_g2_*`) come from the VK and are never validated** — not curve, not subgroup, not field bounds. + +Consequence: if an attacker can pick the VK (issue 1), they can put arbitrary 256-bit garbage in the four G2 limbs. The pairing precompile will simply revert if those limbs aren't valid Fp2 elements, but malicious-yet-valid G2 points (e.g. low-order points outside the prime subgroup) bypass the pairing check entirely. The fix would be a subgroup check via `e(P, [r]G2) == 1`. + +--- + +## 3. MEDIUM — Accumulator limb decomposition uses native `add`/`shl`, not `addmod` + +```yul +lhs_x := add(lhs_x, shl(shift, calldataload(cptr))) // wraps mod 2^256 +... +success := and(success, and(lt(lhs_x, q), lt(lhs_y, q))) +``` + +Each instance limb is only constrained to be `< r ≈ 2^254`. With `num_limbs = 4, num_limb_bits = 68`, the highest limb is `shl(204, limb)`; combined with the `add` (which is mod `2^256`), the limb decomposition is **not injective**. Multiple distinct `(aâ‚€,aâ‚,aâ‚‚,a₃)` tuples produce the same `lhs_x mod 2^256`, and the only later check is `lt(lhs_x, q)`. + +Exploitability requires a circuit that doesn't itself range-check the limbs to `< 2^num_limb_bits` — but several real halo2 accumulator circuits don't, so this is a real foot-gun. A safer encoding would compute `addmod(..., q)` or constrain `shift < 256` and `limb < 2^num_limb_bits` in Solidity. + +--- + +## 4. MEDIUM — `batch_invert` is broken for ≤ 1 element + +```yul +function batch_invert(success, mptr_start, mptr_end, r) -> ret { + let gp := mload(mptr_start) + let mptr := add(mptr_start, 0x20) // mptr_start + 0x20 + for {} lt(mptr, sub(mptr_end, 0x20)) {} {...} + gp := mulmod(gp, mload(mptr), r) // reads PAST mptr_end if size==1 + ... + let inv_first := mulmod(all_inv, mload(second_mptr), r) + let inv_second := mulmod(all_inv, mload(first_mptr), r) + mstore(first_mptr, inv_first) + mstore(second_mptr, inv_second) // writes PAST mptr_end if size==1 +} +``` + +Triggered when `bdfg21::computations` produces `sets.len() == 1` (all queries at the same rotation): `second_batch_invert_end = 0x20`, so `batch_invert(success, 0, 0x20, r)` reads/writes outside its declared range and returns garbage in `r_eval`. The verifier then accepts a math-wise unrelated polynomial relation. + +Doesn't trigger on standard Plonk (which always has cur and next rotations), but a pathological circuit hits it. + +--- + +## 5. LOW — `pop(q)` / `pop(y)` / `pop(delta)` is cosmetic + +```yul +pop(y) +pop(delta) +... +pop(q) +``` + +These don't actually clear anything — they just discard the top of the Yul stack. Given that `q`, `y`, and `delta` are local vars they will go out of scope anyway. Not a vulnerability, but noteworthy in a security review: the pattern looks defensive but isn't. + +--- + +## 6. LOW — Trace verifier is non-`view` and leaks intermediates + +```yul +function verifyProof(...) public {%- if self.trace %} returns (bool) {%- else %} view returns (bool) {%- endif %} +``` + +Trace mode emits `LOG1` events with all challenges (`theta, beta, gamma, y, x, zeta, nu, mu`) and pairing inputs. None are *secret* (everything is derivable from public proof + VK), but if a deployer accidentally ships an artifact rendered with `RenderDiagnostics { trace: true, .. }` in production, they'll burn extra gas on every verification and pollute logs. A `require(false)`-style guard, or refusing to render trace mode without an explicit `unsafe_*` API, would help. + +--- + +## Suggested first attack to demonstrate in the CTF + +If the target instance is the file at `generated/Halo2Verifier.sol` (the un-pinned variant), exploit **issue 1**: + +```solidity +// Malicious VK whose runtime bytecode is the literal 0x4a0 bytes the verifier extcodecopies +contract EvilVK { + constructor() { + assembly { + // craft 37 words of vk_digest/num_instances/k/n_inv/omega/.../g2_*/neg_s_g2_* + // pick g2 == neg_s_g2 to make the pairing trivially satisfiable + mstore(0x00, 0) // vk_digest + mstore(0x20, 0) // num_instances + // ... + mstore(0x1a0, GX1) mstore(0x1c0, GX2) mstore(0x1e0, GY1) mstore(0x200, GY2) // g2 + mstore(0x220, GX1) mstore(0x240, GX2) mstore(0x260, GY1) mstore(0x280, GY2) // neg_s_g2 := g2 + return(0x00, 0x4a0) + } + } +} + +// Forge a proof where pairing_lhs == -pairing_rhs (e.g. both = G1 point, then negate one). +verifier.verifyProof(address(evilVK), forgedProof, fakeInstances); // returns true +``` + +The current `templates/contracts/Halo2Verifier.sol` blocks this path via `AUTHORIZED_VK` + `EXPECTED_VK_CODEHASH`, so on a CTF setup using the new template the attacker has to fall back to issues 3–4 (which need a vulnerable circuit) or to off-template bugs (e.g. a deployer that forgets to pin the VK at construction). + + + Halo2 BLS12-381 Solidity Verifier - Security Audit (informal) + + Scope: src/codegen.rs, src/codegen/{template.rs, util.rs, pcs.rs, evaluator.rs}, src/transcript.rs, src/evm.rs, templates/contracts/Halo2Verifier.sol, + templates/contracts/Halo2VerifyingKey.sol, generated/Halo2Verifier-*.sol. + Curve: BLS12-381 via EIP-2537 precompiles (0x0b / 0x0c / 0x0f). + Out of scope: halo2 prover correctness (vendored vendor/halo2/), revm/Prague EVM precompile implementations, the underlying KZG security assumption. + + ────────────────────────────────────────── + + 1. Findings overview + + ID │ Severity │ Title + -----+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------ + F-1 │ **Critical** │ `static_working_memory_size` keeps the BN254 stride (`n*2+1`); the keccak buffer can overwrite challenges/VK at runtime + F-2 │ High │ Fiat-Shamir desynchronisation when a phase has zero advice columns (Yul does not append the trailing `0x01`) + F-3 │ High │ `read_g1_point` hashes attacker-controlled raw bytes into the transcript even when the EIP-2537 padding check fails + F-4 │ Medium │ Accumulator reconstruction relies on `add` / `shl` (not `addmod`) and on contract-side limb checks, with a misleading dead branch + F-5 │ Medium │ `g1_to_u256s` / `g2_to_u256s` `unwrap()` on the point-at-infinity, panicking VK generation + F-6 │ Medium │ `delta` is a hard-coded scalar with no compile-time agreement check against `bls12381::Fr::DELTA` + F-7 │ Low │ Trace-mode verifier silently returns `bool` instead of reverting and is no longer `view`; deploying it in production breaks callers using `try/catch` + F-8 │ Low │ `pop(y)` / `pop(delta)` are cosmetic and do not clear memory + F-9 │ Low │ `if mload(HAS_ACCUMULATOR_MPTR)` is read from VK but it is not cross-checked against the codegen-side `acc_encoding` + I-10 │ Informational │ Generator forces `vk.cs().num_instance_columns() <= 1` and `Rotation::cur()` only - silent "not yet implemented" panics if violated + I-11 │ Informational │ `n_inv` and `omega_inv_to_l` are never re-derived from `k`/`omega` on chain - a malicious VK that bypasses the codehash pin can lie + I-12 │ Informational │ `mod(hash, r)` introduces ~2^-255 bias; standard practice, kept for completeness + + The mitigations that have already landed (in particular the AUTHORIZED_VK + EXPECTED_VK_CODEHASH pinning at constructor time, commit 54b2943) close the original + "caller-controlled VK" hole from AUDIT.md finding #1, and the EIP-2537 pairing precompile transitively covers G2 subgroup checks (BN254 audit finding #2). + + ────────────────────────────────────────── + + 2. Critical: keccak buffer overruns on wide circuits + + F-1 - `static_working_memory_size` keeps BN254 stride + + Location: src/codegen.rs::SolidityGenerator::static_working_memory_size, around the // Keccak256 input (can overwrite vk) block. + + rust + itertools::max([ + // Keccak256 input (can overwrite vk) + itertools::max(chain![ + self.meta.num_advices().into_iter().map(|n| n * 2 + 1), // <-- BN254 stride + [self.meta.num_evals + 1], + ]) + .unwrap() + .saturating_sub(vk.len() / 0x20), + // PCS computation + pcs_computation, + // Pairing: 2 G1 points (4 words each) + 2 G2 points (8 words each) = 24 + 1 buf + 25, + ]) + .unwrap() + * 0x20 + + After the BLS port a single G1 commitment occupies 4 EVM words, not 2 (cf. EcPoint::range in src/codegen/util.rs and read_g1_point in templates/contracts/Halo2Verifier.sol). The keccak + input that the verifier accumulates between two squeezes therefore needs + + phase_words = num_advices_phase * 4 + 1 (one extra word for the rolling hash at 0x00) + + words, i.e. exactly twice what the formula budgets. The same understatement applies to the W phase (1 + num_rotations * 4), which is not represented at all in the chain. + + The constant vk_mptr = static_working_memory_size becomes the floor of every memory layout in the contract: + + 0x00..hash_mptr_max : keccak input, grows between squeezes + vk_mptr..vk_mptr + vk_len : VK area (mirrored into memory by extcodecopy) + challenge_mptr.. : THETA / BETA / GAMMA / Y / X / NU / MU + theta_mptr + offsets : ACC_LHS, ACC_RHS, X_N, ..., PAIRING_LHS/RHS + + When phase_words > vk_mptr / 0x20 + vk_len / 0x20, the loop body inside read_g1_point + + yul + mstore(hash_mptr, x_hi) + mstore(add(hash_mptr, 0x20), x_lo) + mstore(add(hash_mptr, 0x40), y_hi) + mstore(add(hash_mptr, 0x60), y_lo) + ret2 := add(hash_mptr, 0x80) + + writes through CHALLENGE_MPTR and beyond, overwriting THETA_MPTR / BETA_MPTR / GAMMA_MPTR / Y_MPTR / X_MPTR / NU_MPTR / MU_MPTR. Since challenges are written at squeeze time + (mstore(challenge_mptr, mod(hash, r)) in squeeze_challenge) and read much later (e.g. let theta := mload(THETA_MPTR) inside evaluator::lookup_computations), the corrupted values + silently propagate into the constraint check. + + Concrete trigger + + For the standard Plonk fixture used in test::create_property_standard_plonk_fixture we have vk_len ≈ 60 * 32 and pcs_computation = 12 + num_rotations*4 ≈ 28 words, so the + formula returns 25 * 0x20. With n = 22 advice columns in any phase the corrected formula is + + n * 4 + 1 = 89 words (correct BLS budget) + n * 2 + 1 = 45 words (used by codegen) + saturating_sub(60) = 0 (under-counts) + + and vk_mptr is computed as if 25 working words sufficed. The keccak buffer of phase-1 then extends to address 0x00 + 89 * 0x20 = 0x720, overwriting CHALLENGE_MPTR = vk_mptr + + vk_len ≈ 0x500. The squeezed theta/beta/gamma are clobbered by subsequent read_g1_point writes for phase-2 advices and the verifier ends up evaluating quotient_eval_numer with + attacker-aliased inputs. + + Impact + + • Soundness: The corrupted challenges feed evaluator::gate_computations, permutation_computations, and lookup_computations. An attacker who knows the corruption pattern (purely + a function of public num_advices / num_evals / vk_len) can craft proofs that the verifier folds against the wrong polynomial relation. We did not construct an end-to-end + forgery in scope, but the corruption is deterministic and aligns 32-byte words with THETA_MPTR / BETA_MPTR, so we treat this as an exploitable soundness hole rather than mere + DoS. + • Liveness: Honest proofs from any halo2 circuit with ≥ 22 advices in some phase (every reasonably sized halo2 chip stack: ECDSA, Poseidon, range-check tables, aggregation + circuits) will be silently rejected. + + Recommendation + + 1. Replace n * 2 + 1 with n * 4 + 1 for the BLS path, and add an instances term (1 + num_instances + num_advices_phase1 * 4) for phase 1. + 2. Add the W phase budget (1 + num_rotations * 4). + 3. Add a debug-time assertion in Halo2Verifier::render that the rendered Yul never writes past vk_mptr for any input shape. + 4. Long term, switch to a memory layout where the keccak buffer is allocated after all permanent state (or at a fixed bumped offset) so the formula becomes a one-shot ceiling + rather than an arithmetic obligation. + + ────────────────────────────────────────── + + 3. High-severity findings + + F-2 - Fiat-Shamir mismatch on empty advice phases + + Locations: + • prover: src/transcript.rs::Keccak256Transcript::squeeze_challenge (the if buf_len == 0x20 { Some(1) } branch). + • verifier: phase loop in templates/contracts/Halo2Verifier.sol calling squeeze_challenge (no 0x01 byte) vs. squeeze_challenge_cont (appends 0x01). + + The Rust transcript appends a 0x01 byte every time it is asked to squeeze and buf happens to contain only the previous hash (buf_len == 0x20). The Yul template only emulates + that semantics through squeeze_challenge_cont, which is emitted explicitly by the for-loop + + jinja + challenge_mptr, hash_mptr := squeeze_challenge(challenge_mptr, hash_mptr, r) + {%- for _ in 0..num_challenges[loop.index0] - 1 %} + challenge_mptr := squeeze_challenge_cont(challenge_mptr, r) + {%- endfor %} + + When a phase contains zero advice commitments (num_advices_phase_i == 0) the inner read_g1_point loop does not execute, hash_mptr stays at 0x20, and the squeeze_challenge call + hashes exactly 32 bytes (the previous hash) without the trailing 0x01. The Rust transcript, on the other hand, sees buf_len == 0x20 in that exact same situation and appends + 0x01. The two sides diverge. + + Reachability. halo2 v0.4 allows phases with no advice columns: ConstraintSystemMeta::new derives num_user_advices from cs.advice_column_phase() whose distribution is set per + advice column - if all advices are pinned to phases 0 and 2, num_user_advices = [k0, 0, k2]. The num_advices() chain that is then iterated in the Yul template includes that + explicit zero. Most production circuits today put every column in phase 0, so the bug is dormant; but it ships as a correctness footgun for any upcoming multi-phase circuit + (lookups + custom challenges, RAM sub-arguments, etc.). + + Impact. Honest proofs are rejected (completeness). For soundness, the verifier's challenges become a known function of the prover's challenges (they differ only by the 0x01 + byte), so an adaptive attacker could in principle replay grinding attacks across the two derivations - we did not classify this as fully exploitable but it is a non-zero + soundness erosion. + + Recommendation. Mirror the Rust logic in Yul: if the buffer length at squeeze time equals 0x20 (i.e. nothing has been appended since the last squeeze), call + squeeze_challenge_cont. Concretely, the codegen can detect the empty phase and emit squeeze_challenge_cont instead of squeeze_challenge. + + F-3 - `read_g1_point` hashes raw calldata before validating the EIP-2537 padding + + Location: templates/contracts/Halo2Verifier.sol, read_g1_point. + + yul + function read_g1_point(success, proof_cptr, hash_mptr) -> ret0, ret1, ret2 { + let x_hi := calldataload(proof_cptr) + ... + ret0 := and(success, iszero(shr(128, x_hi))) + ret0 := and(ret0, iszero(shr(128, y_hi))) + mstore(hash_mptr, x_hi) // <-- written even when ret0 = 0 + mstore(add(hash_mptr, 0x20), x_lo) + mstore(add(hash_mptr, 0x40), y_hi) + mstore(add(hash_mptr, 0x60), y_lo) + ... + } + + The four mstores execute regardless of whether the padding check passed. Today this is benign because the verifier reverts via the global if iszero(success) { revert(0, 0) } + long before any precompile observes the corrupted bytes, and the Fiat-Shamir transcript that consumes those bytes will simply produce a wrong challenge on the failing path. + However: + + 1. The transcript hash is consumed inside squeeze_challenge_cont and then influences memory addresses (mload(NU_MPTR), mload(MU_MPTR), etc.) that drive subsequent calldata + reads. A future refactor that turns one of those reads into a load through a derived pointer would let an attacker steer pointer arithmetic with under-validated bytes. + 2. The exact same read_g1_point is reused for the W openings (4 in total), the quotient commitments, and the random commitment - i.e. for every G1 read in the proof. Any later + optimisation that routes x_hi/y_hi through mod / shl arithmetic before the padding check is verified would lose the guarantee. + + Recommendation. Reverse the order: set ret0 before the four mstores, and make the mstores gated on ret0 (or only write (x_lo, y_lo) and zero out the hi halves so the transcript + content is canonical even on failure). + + ────────────────────────────────────────── + + 4. Medium-severity findings + + F-4 - Accumulator limb reconstruction + + Location: templates/contracts/Halo2Verifier.sol, the if mload(HAS_ACCUMULATOR_MPTR) block. + + Two sub-issues: + + 1. Native `add` / `shl` instead of `addmod`. The reconstruction sums limb << shift into (hi, lo) using EVM add. We verified that the typical (num_limbs, num_limb_bits) + configurations (e.g. 4×96, 4×88) keep the bit-ranges disjoint so there is no overflow. But the verifier silently accepts any (num_limbs, num_limb_bits) from the VK, including + pathological ones such as num_limb_bits = 0 (the lt(limb, shl(num_limb_bits, 1)) check becomes lt(limb, 2), accepting only 0 or 1) or num_limb_bits >= 256 (shl(256, 1) == 0, + accepting nothing). Because the VK is now pinned through EXPECTED_VK_CODEHASH, abuse requires bypassing the pin, but a misconfigured trusted setup ceremony could still + encode a foot-gun there. + 2. Misleading dead code. Inside the per-coord switch: + + yul + if and(eq(coord, 1), 0) { dst := add(dst, 0x40) } // never executes (literal 0) + if iszero(or(eq(coord, 1), eq(coord, 3))) { // can never hold inside `case 1` + dst := add(dst, 0x40) + } + + Both branches are unreachable inside the case 1 arm (where coord ∈ {1, 3}). Reviewers can be misled into thinking they actively guard against a path they don't. + + Recommendation. Use addmod for the limb sums (cheap insurance), enforce 1 ≤ num_limb_bits ≤ 128 and num_limbs * num_limb_bits ≤ 384 at the top of the block, and delete the dead + branches. + + F-5 - VK generation panics on identity G1/G2 + + Location: src/codegen/util.rs::g1_to_u256s / g2_to_u256s. + + rust + let coords = ec_point.borrow().coordinates().unwrap(); + + Coordinates::from(...) returns None for the point at infinity. A trusted-setup output with any commitment equal to the identity (e.g. an empty fixed column) crashes the codegen + pipeline. Not exploitable on chain, but fragile against malformed VKs and inconvenient for tooling that wants to generate a verifier first and decide later. Same concern in + transcript.rs::common_point and write_point, which unwrap() after a fallible coordinate extraction. + + Recommendation. Treat the identity as the all-zero EIP-2537 encoding [0; 4] / [0; 8] and propagate a Result instead of panicking. + + F-6 - `delta` constant is hard-coded and only checked off-line + + Location: templates/contracts/Halo2Verifier.sol + + yul + let delta := 3793952369011177517951424454785176000433849974408744014172535497121832470999 // BLS12-381 Fr::DELTA + + We verified the value matches bls12381::Fr::DELTA by running examples/check_delta.rs. However the value is embedded as a literal in the template; nothing in the build forces a + regeneration if halo2curves ships a different DELTA (e.g. after a future curve update). The audit trail in the template has already shown a stale value being silently kept + (4131629893567559867359510883348571134090853742863529169391034518566172092834). + + Recommendation. Inject delta from the codegen side by computing it directly from bls12381::Fr::DELTA at render time, then have the template consume it via the templating engine. + That removes the off-line-only check and makes future curve swaps a one-liner. + + ────────────────────────────────────────── + + 5. Low-severity & informational findings + + F-7 - Trace-mode verifier returns a `bool` instead of reverting + + jinja + function verifyProof(...) public {%- if self.trace %} returns (bool) {%- else %} view returns (bool) {%- endif %} + + In trace mode the function: + • drops view (logs are emitted), + • returns success (1 or 0) instead of revert-ing on failure, + • leaks the entire challenge transcript and the intermediate PAIRING_LHS / PAIRING_RHS via LOG1. + + The challenges are not secret (they are derivable from public proof + VK), but a deployer that ships a trace-enabled render in production changes the failure semantics from + revert to return false. Callers using try { verifier.verifyProof(...) } catch { ... } then silently misclassify rejected proofs as "verifier executed successfully, said no, but + we will treat the absence of revert as success." Recommendation: gate the trace constructor behind an unsafe_* factory and emit a runtime require(false) if a non-trace caller + invokes it. + + F-8 - `pop(y) / pop(delta)` + + Cosmetic. Yul pop only discards the top stack element; the local Yul variable is already destined to go out of scope. No memory is actually cleared. Already noted in AUDIT.md + for the BN254 path; carried over verbatim. + + F-9 - VK-driven `HAS_ACCUMULATOR_MPTR` is not cross-checked + + The verifier blindly trusts the VK constants has_accumulator, acc_offset, num_acc_limbs, num_acc_limb_bits. With the AUTHORIZED_VK codehash pin in place this is fine; without it + (or after an upgrade that forgets to re-pin), a VK contract can lie about has_accumulator = 1 while the Rust generator was constructed without AccumulatorEncoding. The verifier + then drains num_limbs * 4 = 16 instance slots as accumulator limbs, mis-decodes them as G1 points, and feeds garbage to 0x0c / 0x0f. The pairing precompile reverts (no + soundness break), but the user gets an inscrutable failure mode. + + Recommendation. Hash the (has_accumulator, acc_offset, num_acc_limbs, num_acc_limb_bits) tuple into the codehash explicitly (it already is, transitively), and have the codegen + emit a require on the constructor side that the deployer's acc_encoding agrees with vk.has_accumulator. + + I-10 - Silent generator restrictions + + rust + assert_ne!(vk.cs().num_advice_columns(), 0); + assert!(vk.cs().num_instance_columns() <= 1, "Multiple instance columns is not yet implemented"); + assert!(!vk.cs().instance_queries().iter().any(|(_, rotation)| *rotation != Rotation::cur()), ...); + + These are user-facing panics rather than Results. A deployer integrating the codegen into a CI pipeline gets an opaque crash. Convert to Result<…, GeneratorError>. + + I-11 - `n_inv` and `omega_inv_to_l` are never re-derived on chain + + The verifier uses n_inv (= 1/2^k mod r) and omega_inv_to_l (= ω^{-l}) from the VK without recomputing them from k and omega at runtime. With the codehash pin in place this is + fine (the codehash binds the entire VK byte string). Without it, a malicious VK could substitute n_inv := 1 and omega_inv_to_l := 1, which would silently yield a different + Lagrange basis at evaluation time. The pin closes this; we mention it because future "upgradeable VK" deployments would need to be aware that these are non-redundant trust + roots. + + I-12 - Bias in `mod(hash, r)` + + r = 0x73eda7…00000001 ≈ 2^254.86. With a 256-bit hash, the bias is ~2^256 / r - 1 ≈ 2^-254, completely negligible. Standard practice; no action. + + ────────────────────────────────────────── + + 6. Compatibility / hygiene observations + + • proof_to_bls_padded (src/codegen.rs) and the prover-side transcript (src/transcript.rs::common_point) agree byte-for-byte on the 16-zero-byte EIP-2537 prefix per Fp + coordinate. Good. + • g1_to_u256s reads Fq::to_repr() (LE) and reverses; g2_to_u256s reads Fq::to_bytes() (BE). We verified this difference against halo2derive 0.1.0's impl_field! macro: + + rust + fn to_repr(&self) -> Self::Repr { /* hard-coded LE */ } + pub fn to_bytes(&self) -> [u8; …] { /* honors `endian = "big"` setting for Fq */ } + + so the asymmetry is correct, but it is exactly the kind of footgun that breaks silently if a future halo2curves release changes the macro - we recommend asserting Fq::ENDIAN == + BE once at the top of util.rs so a regression would be immediately observable. + + • The trailing mstore(0x00, 1); return(0x00, 0x20) in non-trace mode means the function only ever returns true or reverts; declare it view returns () and rely on revert-or-not + semantics, or drop the bool return entirely. The current shape misleads callers into believing false is a possible return. + + ────────────────────────────────────────── + + 7. Suggested follow-ups (in priority order) + + 1. Fix F-1. Recompute static_working_memory_size for the BLS layout (multiply advice stride by 4, add the W-phase term, add a phase-1 instances term). Add a unit test that + builds a wide circuit (say 64 advices in phase 1) and asserts the rendered Yul never reads/writes through CHALLENGE_MPTR mid-transcript. + 2. Fix F-2. Mirror the Rust buf_len == 0x20 semantics on the Yul side; the cleanest way is to track buffer occupancy in the codegen and emit squeeze_challenge_cont on empty + phases. + 3. Fix F-3. Reorder the writes in read_g1_point so the transcript only sees padded bytes when ret0 is true. + 4. Re-enable the ignored property tests (pbt_solidity_* in src/test.rs) once the BLS prover backend is wired up; this is the only end-to-end witness for soundness assertions. + 5. Add a cargo xtask audit target that re-runs examples/check_delta.rs and asserts the value found in templates/contracts/Halo2Verifier.sol matches at build time. + 6. Tighten the g*_to_u256s helpers to handle the identity case (F-5) and propagate richer errors out of SolidityGenerator::new (I-10). + + ────────────────────────────────────────── + + 8. Summary + + The BLS12-381 / EIP-2537 port is structurally sound: pairing inputs are laid out per spec, the pinned-VK pattern correctly closes the largest historical hole in this codebase, + and the EIP-2537 precompiles transitively cover G2 subgroup checks. The most consequential issue we found, F-1, is a numeric mistake in the static memory layout calculation that + survives from the BN254 era and is large enough to corrupt Fiat-Shamir state on routinely-sized circuits. F-2 breaks completeness for circuits with empty intermediate phases. + F-3 is currently latent but is the kind of memory-vs-validation ordering bug that becomes exploitable under any future refactor. The remaining items are hardening and ergonomics + work. + + Recommended next actions: fix F-1 immediately (it gates re-enablement of the ignored render / pbt tests), schedule F-2/F-3 for the next correctness sprint, and treat F-4 through + F-9 as code-review polish. + + Plan updated + +⛬ The audit found one critical issue (F-1: BN254-era stride n*2+1 in static_working_memory_size can overflow keccak buffer into challenge memory on circuits with ≥22 advices in + any phase), two high-severity issues (F-2: Fiat-Shamir mismatch on empty advice phases; F-3: read_g1_point hashes raw calldata before validating EIP-2537 padding), and a handful + of medium/low items around accumulator decoding, identity-point handling, the hard-coded delta constant, and trace-mode footguns. The pinned-VK + EXPECTED_VK_CODEHASH pattern + correctly closes the original BN254 caller-controlled-VK hole, and EIP-2537 transitively gives the G2 subgroup check that the BN254 path was missing. + +## 2026-05-02 production-readiness findings: IVC verifier artifact + +Scope note: these findings are about the generated IVC verifier artifact and +its production hardening. Challenge truncation is intentionally out of scope +for this note: the compiler keeps that behavior optional, and this review +looked at an IVC build that enables the option. + +Conclusion: the verifier is not production-ready yet. Several issues are +deployment-integrity, fail-closed, or integration hazards rather than immediate +cryptographic breaks, but they should be closed before shipping. + +### High-priority findings + +#### A-1. Production builds must pin the quotient evaluator hash and length + +Severity: High. + +The verifier pins the VK with an expected runtime length and codehash. The +quotient evaluator must be treated the same way, because the external quotient +contract reconstructs the batched identity numerator, the linearization +expected evaluation, and the simple-selector accumulators. It is not merely a +gas optimization. + +The template now supports an `EXPECTED_QUOTIENT_LENGTH` / +`EXPECTED_QUOTIENT_CODEHASH` path, but the fallback path still accepts any +non-empty quotient evaluator in the constructor and pins whatever codehash was +passed: + +```solidity +require(authorizedQuotient.code.length != 0, "invalid quotient"); +AUTHORIZED_QUOTIENT_CODEHASH = authorizedQuotient.codehash; +``` + +Impact: + +- A malicious or mistaken deployment can wire a quotient evaluator that returns + forged `QUOTIENT_EVAL_MPTR` and selector accumulators. +- The rest of the verifier can then verify a different statement than the one + implied by the intended circuit and VK. +- This is especially dangerous for factories, registries, copied deployment + scripts, and third-party verifier addresses. + +Recommendation: + +- Generate and deploy production IVC verifiers only through the hard-coded + quotient hash path. +- Make `EXPECTED_QUOTIENT_LENGTH` and `EXPECTED_QUOTIENT_CODEHASH` mandatory + for external quotient evaluator builds. +- Add a negative deployment test that rejects a quotient evaluator with the + same ABI and different runtime bytecode. + +#### A-2. Invalid G1/G2 points can burn almost all supplied gas + +Severity: High. + +The verifier forwards `gas()` to EIP-2537 precompiles: + +```yul +staticcall(gas(), 0x0c, ...) +staticcall(gas(), 0x0f, ...) +``` + +EIP-2537 specifies that failed precompile calls burn all gas supplied to the +precompile. It also requires errors for invalid encodings, non-curve points, +points outside the subgroup for MSM/pairing, and invalid input length. The +verifier range-checks G1 coordinates before transcript absorption, but +curve-membership and subgroup checks are deferred to MSM/pairing precompiles. + +Impact: + +- A caller can submit field-canonical but off-curve proof points and cause a + later G1MSM or pairing call to burn nearly all remaining gas. +- If users call the verifier directly this is a UX/DoS issue. If another + protocol calls the verifier, the gas griefing is more serious. + +Recommendation: + +- Use bounded-gas wrappers for EIP-2537 calls, sized from the expected valid + input length plus a safety margin. +- Check exact return sizes after every precompile call. +- Add negative tests for off-curve G1, wrong-subgroup G1 in MSM, malformed G2, + and invalid pairing inputs. + +#### A-3. Accumulator RHS fixed-base scalar handling must be schema-checked + +Severity: High for general codegen, low/medium for the current IVC artifact. + +The accumulator block documents an RHS layout containing point limbs, one +scalar, and fixed-base scalars in `BTreeMap` key order. The current code walks +`fixed_scalar_ptr` only when generated fixed-base points exist; for the concrete +IVC layout there appears to be no tail, so the current artifact is probably +consistent. The template still needs an explicit schema assertion. + +Impact: + +- A future circuit with fixed-base accumulator terms could silently verify the + wrong accumulator equation if the codegen metadata, comments, and instance + layout diverge. +- Without a schema assertion, a layout mismatch fails unclearly. + +Recommendation: + +- For the concrete IVC verifier, assert that the accumulator layout consumes + exactly the expected instance words. +- For general codegen, either implement the documented fixed-base tail MSM for + every generated fixed-base term or make the zero-tail invariant explicit in + VK/codegen metadata and generated tests. + +#### A-4. Accumulator limb decoding is non-canonical + +Severity: High/Medium. + +`load_acc_coord_shifted` extracts packed accumulator limbs with masks: + +```yul +let limb := and(shr(mul(mod(i, limbs_per_word), bits), packed), mask) +``` + +For 56-bit limbs, a coordinate is split across words with unused high bits. The +verifier checks each public-input word is `< Fr`, but it does not reject unused +high bits in the packed words. Those bits are silently ignored by the +accumulator decoder. + +Impact: + +- Multiple public-input encodings can decode to the same BLS point. +- The transcript absorbs the full public-input words, so this is not + automatically a bypass, but it is a canonicality hole. +- It can become a soundness bug if the circuit-side public-input packing does + not enforce the same unused-bit zero constraints. + +Recommendation: + +- Add verifier-side checks that unused high bits are zero for every packed + accumulator coordinate word. +- For the identity encoding, accept only the exact full-point identity form and + require strict packing for all non-identity coordinates. +- Add negative tests for high unused accumulator bits. + +### Medium-priority findings + +#### A-5. Gas checkpoints must not be present in production builds + +Severity: Medium. + +`gas_checkpoint()` emits `LOG1` repeatedly during verification when the gas +checkpoint build path is enabled. That makes the verifier non-`view`, prevents +safe `staticcall` usage, leaks profiling artifacts into logs, and adds gas to +every proof. + +Implementation note: the checkpoint helper and all call sites are Askama +branches in `templates/contracts/Halo2Verifier.sol`, guarded by `self.gas_checkpoints`. +Default render paths set this from the `solidity-gas-checkpoints` Cargo feature, +while `RenderDiagnostics { gas_checkpoints: true, .. }` forces it on for benchmarking. The +separate `Halo2QuotientEvaluator.sol` template does not currently emit +checkpoint logs; it copies the verifier frame, runs the quotient numerator +block, and returns the fixed output frame. + +Recommendation: + +- Keep gas checkpoints only in explicit debug/bench artifacts. +- Ensure production generation disables trace/gas flags and emits: + +```solidity +function verifyProof( + bytes calldata proof, + uint256[] calldata instances +) external view returns (bool) +``` + +#### A-6. The Solidity pragma is too loose for the generated opcode set + +Severity: Medium. + +The generated templates currently use: + +```solidity +pragma solidity ^0.8.0; +``` + +The verifier uses Yul `mcopy`, which requires compiler support for Cancun-era +opcodes. The generated source should require a compiler version that supports +the emitted Yul and should be compiled for an EVM target that supports both +MCOPY and EIP-2537 on the destination chain. + +Recommendation: + +- Pin the generated templates to at least Solidity `^0.8.24`, or a narrower + exact compiler version used by CI. +- Compile with the intended EVM version and record the compiler/EVM target in + deployment artifacts. + +#### A-7. Invalid proofs mostly revert despite a bool-returning API + +Severity: Medium. + +The public API returns `bool`, but many invalid proof paths use +`revert(0, 0)`. Only some early deployment-binding checks return `false`. + +Impact: + +- Integrators may expect invalid proofs to return `false`, while malformed + proofs, invalid scalars, invalid points, denominator failures, bad quotient + output, and bad pairings mostly revert. +- The API is harder to safely compose in other contracts. + +Recommendation: + +- Decide on one policy: + - revert on all invalid proofs and document that behavior; or + - consistently return `false`. +- Prefer custom errors or clear NatSpec if reverting remains the production + policy. + +#### A-8. The manual ABI parser should check dynamic offsets + +Severity: Medium. + +The verifier hardcodes calldata locations such as: + +```yul +PROOF_LEN_CPTR +PROOF_CPTR +NUM_INSTANCE_CPTR +INSTANCE_CPTR +``` + +It should also assert the dynamic ABI heads match the hand-rolled parser: + +```yul +success := and(success, eq(calldataload(0x04), 0x40)) +success := and(success, eq(calldataload(0x24), EXPECTED_INSTANCE_HEAD)) +``` + +Recommendation: + +- Add canonical ABI-offset checks for `proof` and `instances`. +- Alternatively, use Solidity's `proof.offset`, `proof.length`, + `instances.offset`, and `instances.length` values directly in the assembly. + +#### A-9. Check the quotient evaluator against EIP-170 in CI + +Severity: Medium. + +The external quotient evaluator is close to Ethereum's `0x6000` runtime +code-size limit. Contract creation fails when returned runtime code exceeds +24,576 bytes. + +Recommendation: + +- Compile the exact production evaluator with the intended optimizer and EVM + target in CI. +- Assert deployed runtime length is below 24,576 bytes with margin. +- Split the evaluator or move static data if the margin becomes too small. + +### Lower-priority hardening + +#### A-10. Comments still describe compressed proof commitments + +Severity: Low. + +Some template comments still describe proof commitments as zcash-compressed +points that are decompressed inline. The current verifier expects +EIP-2537-padded uncompressed G1 points in calldata and hashes that 128-byte +form into the transcript. + +Recommendation: + +- Update stale comments around proof serialization, transcript absorption, and + commitment memory layout. +- Treat serialization comments as security-sensitive because off-chain repack + bugs cause hard-to-debug transcript mismatches. + +#### A-11. Add generated layout invariant tests + +Severity: Low. + +This verifier is layout-sensitive. A cheap generated test suite should assert +relationships such as: + +```text +PROOF_CPTR + proof_len == NUM_INSTANCE_CPTR +INSTANCE_CPTR == NUM_INSTANCE_CPTR + 0x20 +ADVICE_COMMS_MPTR_BASE + advice_count * 0x80 == LOOKUP_M_COMMS_MPTR_BASE +``` + +Recommendation: + +- Generate Foundry/Hardhat or Rust-side tests for the rendered constants. +- Keep these as CI checks rather than runtime production assertions unless the + cost is negligible. + +#### A-12. Remove unused constants and dead paths + +Severity: Low. + +Examples observed in generated or adjacent code include unused quotient cursor +constants, scratch constants, and stale helper paths. + +Recommendation: + +- Remove dead constants and helpers from production renders. +- Keep debug-only helpers behind explicit trace/bench flags. + +### Things that looked internally consistent + +- The proof parser's section lengths appear internally consistent for the IVC + artifact. +- VK runtime length and verifier `extcodecopy` layout agree in the generated + artifact. +- EIP-2537 coordinate canonicality checks reject non-zero top padding bytes and + accept coordinates up to `p - 1`. +- The final KZG pairing argument order appears consistent with the helper's + `e(arg0, G2_BASE) * e(arg1, NEG_S_G2_BASE) == 1` convention. + +### Recommended action list + +1. Make expected quotient codehash/length mandatory for external quotient + evaluator production builds. +2. Remove gas checkpoints from production artifacts and make production + `verifyProof` `external view`. +3. Add bounded-gas wrappers and exact return-size checks for EIP-2537 calls. +4. Add canonical packed-limb checks for accumulator public inputs. +5. Assert accumulator schemas consume all expected instance words, or implement + the documented fixed-base tail in general codegen. +6. Require a Solidity compiler/EVM target that supports the emitted Yul and + target-chain precompiles. +7. Add ABI dynamic-offset checks for the hand-rolled parser. +8. Add CI checks for quotient evaluator runtime size under EIP-170. +9. Add negative tests for malformed/off-curve/wrong-subgroup points, high + accumulator padding bits, wrong quotient codehash, wrong ABI offsets, wrong + proof length, and mutated quotient output. + +References: + +- EIP-2537: https://eips.ethereum.org/EIPS/eip-2537 +- Solidity 0.8.24 release notes: https://www.soliditylang.org/blog/2024/01/26/solidity-0.8.24-release-announcement/ +- Ethereum Pectra announcement: https://blog.ethereum.org/2025/04/23/pectra-mainnet +- EIP-170: https://eips.ethereum.org/EIPS/eip-170 + +## 2026-05-02 follow-up audit notes: accumulator and batching edge cases + +I found no obvious "any random proof passes" bug in the main KZG pairing path, +but this verifier should still not be treated as production-ready without +resolving the items below. The highest-risk items are accumulator handling and +challenge truncation, because both can silently make the Solidity verifier +check a different statement than the native Midnight/Halo2 verifier. + +### Findings overview + +| Severity | Issue | Why it matters | +| --- | --- | --- | +| Resolved / clarified | Accumulator RHS fixed-base tail appears documented but not verified | The current IVC verifier fully collapses the carried proof accumulator, so no fixed-base scalar tail remains in public instances. The generator now renders the no-tail layout explicitly and emits `fixed_scalar_ptr` only for future non-collapsed layouts with generated fixed bases. | +| Resolved / confirmed mirrored | `x1` and `x4` powers are truncated to 128 bits, not just `x3` | Confirmed against Midfall `proofs/src/poly/kzg/mod.rs`: with `truncated-challenges`, Rust truncates x3 directly and uses `truncated_powers(x1)` / `truncated_powers(x4)` for PCS batching. Solidity intentionally mirrors this by masking stored powers, while keeping x1/x4 accumulators full precision. | +| Resolved / debug-only | Gas logging is live in production path | Production renders do not emit `gas_checkpoint()` and keep `verifyProof` as `external view`. Checkpoints are available only through the `solidity-gas-checkpoints` feature or explicit gas-checkpoint render helpers. | +| Clarified / integration requirement | Raw `verifyProof` does not bind application semantics | The raw generated verifier intentionally checks only "this proof is valid for these public instances under this VK." Generated NatSpec now requires wrappers to bind state roots, program ID, expected IVC output, chain/domain, and related application semantics. | +| Resolved / deployment guard | Precompile assumptions should be explicit | Constructors now run a deployment-time smoke test for EIP-2537 G1ADD, G1MSM, and pairing using identity inputs, and generated comments state the Solidity/EVM target requirement. | +| Resolved / fail-fast | Malformed calldata and failed `success` states keep executing expensive work | ABI/proof/instance shape failures now revert before transcript parsing, Lagrange failures revert before quotient reconstruction, and EIP-2537 calls are guarded with `if success` instead of `and(success, staticcall(...))`. | +| Clarified / plan-enforced | Point validation is indirect | `common_uncompressed_g1` checks canonical Fp encoding before transcript absorption, while curve/subgroup validation is delegated to EIP-2537 G1MSM/pairing. `ProtocolPlan::validate` rejects absorbed proof commitments that are not opened/consumed, and ignored negative tests mutate every proof G1 to non-canonical/off-curve encodings. | + +### F-1. Accumulator fixed-base terms look omitted + +Status: Resolved / clarified. + +This block is suspicious: + +```solidity +// RHS layout: point limbs (x,y), scalar, then fixed-base +// scalars in BTreeMap key order (`-G`, fixed_i, perm_i ...) +let fixed_scalar_ptr := add(rhs_scalar_ptr, 0x20) +let acc_msm_len := sub(acc_pair_ptr, acc_scratch) +``` + +The current IVC verifier fully collapses the carried proof accumulator before it +is exposed as public input. That means the on-chain accumulator schema is: + +```text +acc_offset + LHS point limbs x/y, LHS scalar + RHS point limbs x/y, RHS scalar +``` + +No `-G`, fixed commitment, or permutation commitment scalar tail remains for +this verifier. The generator now states this directly in the rendered +`Halo2Verifier.sol` comments and renders `fixed_scalar_ptr` only when +`acc_fixed_bases.len() > 0`, which is reserved for future partially-collapsed +layouts. The instance-count check remains explicit: + +```solidity +acc_expected_words = acc_offset + lhs_point + lhs_scalar + rhs_point + rhs_scalar + fixed_tail_len +``` + +For the current fully-collapsed IVC layout, `fixed_tail_len == 0`. + +This does not mean the decider proof has no fixed bases anywhere. The final +KZG pairing check still uses fixed, permutation, quotient, advice, lookup, and +accumulator commitments as verifier-key/proof bases in the PCS MSM. The +clarification is narrower: those bases are not encoded as an extra +public-input scalar tail on `ACC_RHS_MPTR` for the carried proof accumulator. + +### F-2. Challenge truncation looks inconsistent + +Status: Resolved / confirmed mirrored. + +This comment says: + +```solidity +// truncated-challenges: x3 is the f_com evaluation point ... +// midnight-proofs truncates it to 128 bits at squeeze time +mstore(X3_MPTR, and(mload(X3_MPTR), 0xffffffffffffffffffffffffffffffff)) +``` + +But later the verifier also truncates powers of `x1`: + +```solidity +acc := mulmod(acc, x1, r) +mstore(p, and(acc, 0xffffffffffffffffffffffffffffffff)) +``` + +and powers of `x4`: + +```solidity +x4_pow_full := mulmod(x4_pow_full, x4, r) +let x4_pow_1 := and(x4_pow_full, 0xffffffffffffffffffffffffffffffff) +``` + +This is not the same as "the challenge is 128-bit." It uses the low 128 bits of +each emitted Fr power. That behavior has now been checked against the native +source of truth: + +- `midfall/proofs/src/poly/kzg/mod.rs` truncates `x3` directly when + `truncated-challenges` is enabled. +- The same Rust file builds `powers_x1` with `truncated_powers(x1)`. +- The final commitment/evaluation fold uses `truncated_powers(x4)`. + +The Solidity code mirrors that shape: `x1` and `x4` themselves remain full +squeezed Fr words, their internal power accumulators remain full precision, and +only the emitted batching powers are masked to 128 bits before use. Existing +trace hooks cover the squeezed challenges, `f_eval`, `v`, `final_com`, and +pairing inputs; richer per-power trace IDs would still be useful diagnostics, +but the current masks are not a Solidity-only divergence. +The remaining protocol/documentation item is to state the resulting 128-bit +batching-soundness target wherever the IVC verifier profile is described. + +### F-3. Gas logging should be a separate trace build + +Status: Resolved / debug-only. + +The verifier may emit logs when compiled with gas checkpoints: + +```solidity +function gas_checkpoint(id) { + log1(0, 0, or(shl(248, id), gas())) +} +``` + +Production artifacts do not include this. The generated template gates the +function and every call site behind `self.gas_checkpoints`; the default renderer +sets that flag from the optional `solidity-gas-checkpoints` feature, while +explicit `RenderDiagnostics { gas_checkpoints: true, .. }` options force it for +benchmarking. + +Live checkpoint logs would have three production problems: + +1. `verifyProof` cannot be safely exposed as `view`. +2. Any contract using `staticcall` to query verification will fail because + `LOG` is not allowed in a static context. +3. Logs add recurring gas and noisy events. + +The production ABI remains `external view returns (bool)` unless trace or gas +checkpoint output is intentionally enabled. + +### F-4. Raw `verifyProof` does not bind application semantics + +Status: Clarified / integration requirement. + +The generated `verifyProof(bytes,uint256[])` ABI verifies only that the supplied +proof is valid for the supplied public instances under this pinned verifier key +and protocol layout. It does not know what a particular application's first +non-accumulator instance means. + +This is intentional for a reusable generated verifier, but it must be handled by +the application wrapper. The wrapper must bind the expected state roots, program +identifier, expected IVC output, chain/domain separation, and any +protocol-specific authorization before treating a successful proof as meaningful +for that application. The generated NatSpec now states this explicitly. + +### F-5. Precompile assumptions should be explicit + +Status: Resolved / deployment guard. + +The code depends on EIP-2537 addresses: + +```text +0x0b BLS12_G1ADD +0x0c BLS12_G1MSM +0x0f BLS12_PAIRING_CHECK +``` + +The EIP defines those addresses and the 64-byte Fp encoding rules, including +canonical field-element validation. The generated verifier constructor now runs +a deployment-time smoke test: + +- `G1ADD(identity, identity)` must return the 128-byte identity encoding. +- `G1MSM([(identity, 0)])` must return the 128-byte identity encoding. +- `PAIRING_CHECK([(identity_g1, identity_g2)])` must return the 32-byte value + `1`. + +This catches absent precompile implementations, short return data, and obviously +incompatible semantics before the verifier can be deployed. The source comments +also state the compiler/chain requirement: Solidity `>=0.8.24` and a target EVM +supporting MCOPY and EIP-2537. + +One item not flagged: using `sub(r, v)` as an MSM scalar can produce `r` when +`v == 0`, but EIP-2537 scalars for multiplication are not required to be less +than the subgroup order. + +### F-6. Make failed parsing fail earlier + +Status: Resolved / fail-fast. + +Malformed ABI/proof length sets `success`, but parsing can continue: + +```solidity +success := and(success, eq(0x1e60, calldataload(PROOF_LEN_CPTR))) +... +if iszero(success) { revert(0, 0) } +``` + +For bad calldata, the verifier could still perform many transcript reads before +reverting. Later, failed states could still evaluate expensive `staticcall` +expressions because Yul builtins are not short-circuiting. + +Resolution: + +- After VK header, proof length, instance count, and calldata size checks, the + verifier now immediately reverts. +- If Lagrange/common-polynomial setup fails, the verifier now reverts before + external quotient reconstruction. +- EIP-2537 calls in the PCS and accumulator paths now use: + +```solidity +if success { + success := staticcall(...) +} +``` + +instead of: + +```solidity +success := and(success, staticcall(...)) +``` + +This prevents already-failed verifier states from entering G1MSM/G1ADD/pairing +precompiles. + +### F-7. Point validation and targeted negative tests + +Status: Clarified / plan-enforced, with remaining negative-test backlog. + +`common_uncompressed_g1` validates canonical EIP-2537 Fp encoding before a proof +point is absorbed into the transcript. It intentionally does not run a separate +curve/subgroup precompile call at read time. Instead: + +- `ProtocolPlan::validate` enforces that absorbed proof advice commitments are + opened by PCS; the other absorbed proof commitment categories are generated + into the PCS or accumulator MSM/pairing paths. +- EIP-2537 G1MSM and pairing perform the actual curve/subgroup validation for + those consumed points. +- Ignored EVM-heavy tests already mutate every proof G1 into non-canonical and + off-curve encodings and assert both native/Solidity rejection paths. + +Remaining useful negative tests: + +1. Flip each proof G1 into a wrong-subgroup point if one can be generated; + every mutation must revert. +2. Set every public instance once to `r`; each Fr value + must reject. +3. Mutate high bits of `x1`/`x4`-power-dependent openings; the result must + disagree with Rust if masks are wrong. +4. Malform accumulator identity encodings: + - x identity flag with nonzero y; + - `p - 1` without identity flag; + - unused high bits in packed limb words. +5. Add a nonzero accumulator fixed-base tail test if the tail is part of the + real IVC relation. +6. `staticcall` the production verifier; it should succeed once logging is + removed. + +The two items to resolve first are the accumulator RHS layout mismatch and the +`x1`/`x4` truncation. Those are the most likely to become real soundness or +"Solidity verifies a different protocol" bugs. + +Reference: + +- EIP-2537: https://eips.ethereum.org/EIPS/eip-2537 + +## 2026-05-02 continuation review findings + +Scope: follow-up static review of the current working tree, with emphasis on +CTF-relevant compiler shape mismatches and deployable generated artifacts. + +### R-1. Unsupported instance columns collapse into one eval + +Severity: High. + +Relevant code: + +- `src/codegen/evaluator.rs`: `instance_eval_at` +- `templates/contracts/Halo2Verifier.sol`: instance transcript absorption and Lagrange + evaluation prologue +- `src/generator/api.rs`: `try_new` and `GeneratorConfig` + +The generator accepts up to two instance columns, but every non-committed +instance query is mapped to the single `INSTANCE_EVAL_MPTR` value. The Solidity +ABI/transcript also absorbs one flat `instances` array, not one length/value +stream per instance column. + +Impact: + +- A circuit with two non-committed instance columns can be compiled into a + verifier for a different public-input statement. +- Constraints querying distinct non-committed instance columns can be evaluated + against the same Lagrange-combined instance value. +- This is a miscompiler for unsupported shapes, even if the current zkstdlib + fixture uses the narrow one committed / one non-committed split. + +Recommendation: + +- Reject generation unless + `vk.cs().num_instance_columns() - num_committed_instances == 1`. +- Alternatively, implement per-column instance calldata layout, transcript + absorption, Lagrange evaluation buffers, and expression/quotient plumbing. + +### R-2. Committed instance commitments are hard-coded as identity + +Severity: High. + +Relevant code: + +- `templates/contracts/Halo2Verifier.sol`: unconditional `committed_pi = + G1Affine::identity()` absorption +- `src/codegen/util.rs`: `committed_instance_comms` +- `src/codegen/pcs.rs`: committed-instance PCS query construction +- `src/generator/api.rs`: `GeneratorConfig::num_committed_instances` + +The Solidity transcript always absorbs exactly one identity commitment, and the +codegen points every committed-instance PCS query at `G1_IDENTITY_MPTR`. This +only matches the narrow zkstdlib path with one identity committed instance. + +Impact: + +- Non-identity committed public inputs are not represented. +- Zero committed inputs or multiple committed columns diverge from the native + transcript schedule. +- Generated verifiers can silently verify a different Fiat-Shamir / PCS + statement than the circuit author intended. + +Recommendation: + +- Hard-reject unsupported `num_committed_instances` values and document the + identity-only committed instance mode. +- Or extend the ABI/codegen to pass the actual committed commitment list, absorb + the exact count in the transcript, and use those commitments in PCS queries. + +### R-3. Stale generated verifiers still miss precompile return checks + +Severity: Medium. + +Relevant code: + +- `generated/Halo2Verifier-10.sol`: `ec_add_acc`, `ec_mul_acc`, + `ec_add_tmp`, `ec_mul_tmp`, and `ec_pairing` +- Other checked-in `generated/Halo2Verifier-*.sol` artifacts with the same + helper shape + +The current templates include much better EIP-2537 precompile return-size +checks and constructor smoke tests, but the checked-in generated verifier +artifacts are stale. They still call precompiles using only the `staticcall` +success bit, then consume output memory without requiring the expected +`returndatasize()`. + +Impact: + +- On chains or local forks without EIP-2537 at the expected addresses, calls to + empty accounts can succeed with empty returndata and stale memory. +- If a stale generated artifact is deployed as the CTF target, invalid proofs + may fail open in wrong-chain or inaccurate-test-environment conditions. + +Recommendation: + +- Regenerate all committed `generated/*.sol` artifacts from the fixed templates. +- Remove stale generated verifier artifacts from deployable outputs if they are + not intended to be used. +- Add a check in CI that generated artifacts contain the same precompile + return-size and smoke-test guards as the templates. + +### R-4. Accumulator zero scalars skip point validation + +Severity: Medium. + +Relevant code: + +- `templates/contracts/Halo2Verifier.sol`: LHS accumulator scalar switch +- `templates/contracts/Halo2Verifier.sol`: RHS accumulator scalar switch +- `templates/contracts/Halo2Verifier.sol`: `load_acc_point` + +`load_acc_point` range-decodes accumulator points, but curve/subgroup +validation is deferred until the point reaches an EIP-2537 MSM or pairing call. +When the public scalar is zero, the LHS path normalizes the decoded point to +identity and never sends the supplied point to a precompile; the RHS path also +drops a zero-scalar variable-base contribution. + +Impact: + +- Arbitrary in-field coordinates can be accepted as an equivalent zero + contribution when the associated scalar is zero. +- If consumers bind raw public-input bytes, this creates calldata malleability + around accumulator encodings. +- If the circuit/application expects canonical accumulator points even for + zero-scalar terms, the Solidity verifier accepts encodings outside that + intended language. + +Recommendation: + +- Validate decoded accumulator points before applying the scalar switch. +- Or explicitly require the canonical identity encoding whenever the associated + scalar is zero. + +## 2026-05-03 review findings: Midfall verifier translation + +Scope: `templates/contracts/Halo2Verifier.sol`, the Halo2/Midnight Solidity verifier +code generator, and generated verifier behavior as a translation of the native +Midfall verifier path in `../midfall/proofs/src/plonk/verifier.rs` and its +dependencies. + +### Findings overview + +| ID | Severity | Title | +| --- | --- | --- | +| F-1 | Medium | Identity committed instance is an implicit security boundary | +| F-2 | Low | Accumulator encoding accepts invalid limb sizes | +| F-3 | Low | Proof commitment plan records phase-sorted advice columns ambiguously | + +### F-1. Identity committed instance is an implicit security boundary + +Severity: Medium. + +Relevant code: + +- `templates/contracts/Halo2Verifier.sol`: committed-instance transcript absorb block +- `src/codegen/util.rs`: `committed_instance_comms` points at + `G1_IDENTITY_MPTR` + +The generated verifier always absorbs and opens the committed instance column as +G1 identity. That matches the documented IVC shape, but the generator cannot +prove from the VK that future proofs or call sites are using identity committed +instances. If this code is reused for a Rust verifier path that supplies +non-identity committed instance commitments, Solidity verifies a different +transcript / PCS statement than the Rust verifier. + +Impact: + +- Non-identity committed public inputs are not represented in the Solidity ABI. +- Reusing the generator outside the documented identity-committed-column shape + can silently change the verified statement. +- The behavior is safe only while the integration boundary remains explicit and + enforced. + +Recommendation: + +- Make the identity-committed-instance mode explicit in the API/type name and + documentation. +- Or extend the ABI/codegen to accept committed-instance commitments as verifier + inputs and use them in both transcript absorption and PCS queries. + +### F-2. Accumulator encoding accepts invalid limb sizes + +Severity: Low. + +Relevant code: + +- `src/codegen/mod.rs`: `AccumulatorEncoding::new` +- `src/codegen/generator.rs`: accumulator layout calculations using + `254 / acc_encoding.num_limb_bits` + +`AccumulatorEncoding::new` stores `num_limb_bits` without validation, but render +paths later compute `254 / num_limb_bits`. Passing zero panics during +generation, and unsupported limb widths are only caught indirectly by generated +code checks. + +Impact: + +- Invalid generator configuration can cause a render-time panic. +- Unsupported accumulator encodings are rejected late and inconsistently. + +Recommendation: + +- Validate `num_limb_bits != 0` when constructing accumulator encoding. +- Prefer validating the currently supported `7x56` encoding through the + constructor/config path before rendering. + +### F-3. Proof commitment plan records phase-sorted advice columns ambiguously + +Severity: Low. + +Relevant code: + +- `src/codegen/protocol.rs`: `proof.commitments.extend(... Advice { column })` + +`advice_indices` is the phase-sorted proof order, but each entry is stored as +`CommitmentRead::Advice { column }`. Today the vector appears to be used only +for aggregate accounting, so this is latent. If future proof parsing, tracing, +or validation iterates `proof.commitments` as canonical proof order, nontrivial +advice phases can bind the wrong original column. + +Impact: + +- Current generated verifier behavior does not appear exploitable from this + vector alone. +- Future consumers may misinterpret proof order versus original advice-column + index. + +Recommendation: + +- Store both proof-order index and original advice-column index, or rename the + field to make the phase-sorted interpretation explicit. +- Add a regression test with nontrivial advice phases before introducing new + consumers of `ProofReadPlan::commitments`. + +## 2026-05-03 Rust codebase review findings + +Scope: Rust correctness, performance, idioms, maintainability, error handling, +and security review of the current verifier-generator codebase. + +### Findings overview + +| ID | Severity | Area | Summary | +| --- | --- | --- | --- | +| RF-1 | P2 | Error handling | Return errors for malformed proof repacking | +| RF-2 | P2 | Error handling | Propagate render planning failures | +| RF-3 | P2 | Reproducibility | Pin verifier-critical Git dependencies | +| RF-4 | P3 | Configuration | Remove legacy codegen process configuration | +| RF-5 | P3 | API clarity | Use the generator parameter or remove it | +| RF-6 | P3 | Performance | Cache the transcript modulus | + +### RF-1. Return errors for malformed proof repacking + +Severity: P2. + +Relevant code: + +- `src/codegen/generator.rs:2968` + +`SolidityGenerator::repack_proof` is public and sits on the boundary between +user-supplied native proof bytes and generated calldata. It now returns +`Result, RepackError>` for length mismatches and bad compressed G1 +encodings instead of panicking. + +Recommendation: + +- Keep relayer/service callers on the `Result`-returning API and propagate the + typed error. + +### RF-2. Propagate render planning failures + +Severity: P2. + +Relevant code: + +- `src/codegen/generator.rs:2904` + +The public render methods return `Result<_, fmt::Error>`, but layout/planning +failures are converted to panics here and in nearby VK layout paths. A large or +unsupported circuit can unwind a library caller instead of yielding an +actionable error. + +Recommendation: + +- Introduce a render/codegen error type and propagate layout, payload, and + config failures with `?`. + +### RF-3. Pin verifier-critical Git dependencies + +Severity: P2. + +Relevant code: + +- `Cargo.toml:11` + +The Midfall dependency is resolved from a mutable branch, and this repo ignores +`Cargo.lock`, so verifier generation can change as the branch moves. The local +`.cargo` path patch also means local checks may not match a clean clone. + +Recommendation: + +- Pin a `rev` and track the lockfile for repo CI, or vendor the exact audited + dependency set. + +### RF-4. Remove Legacy Codegen Process Configuration + +Severity: resolved. + +The legacy process parser was removed. Quotient-generation choices now flow +through explicit `CodegenConfig` values, so process state cannot silently +change or fail to change generated verifier bytecode. + +### RF-5. Use the generator parameter or remove it + +Severity: P3. + +Relevant code: + +- `src/codegen/mod.rs:218` + +The old crate-level calldata helper was removed. The normal path is now +`SolidityGenerator::encode_calldata`, which uses the bound proof layout and +checks the configured public-instance count before ABI encoding. + +Recommendation: + +- Use the generator method for production calldata. The low-level + `evm::encode_calldata` helper remains available for tests that intentionally + build malformed/raw calls. + +### RF-6. Cache the transcript modulus + +Severity: P3. + +Relevant code: + +- `src/transcript.rs:108` + +Each challenge squeeze reparses `Fq::MODULUS` from hex into a `U256`. This is +small but on the transcript hot path and easy to avoid. + +Recommendation: + +- Cache the modulus with `std::sync::LazyLock`, `OnceLock`, or constant limbs. + +## 2026-05-03 added review findings + +### Finding 1. Zero-scalar accumulator points skip curve validation + +Severity: P3. + +Relevant code: + +- `templates/contracts/Halo2Verifier.sol:1537-1545` + +`load_acc_point` only range-decodes accumulator public inputs; the EIP-2537 +curve/subgroup check happens later only when the point is sent to +G1MSM/G1ADD/pairing. When `lhs_scalar` is zero the verifier overwrites the +decoded point with identity, and when `rhs_scalar` is zero it omits the +variable-base point, so malformed off-curve accumulator coordinates can be +accepted as unused identity contributions. + +Recommendation: + +- If public accumulator encodings are part of the application statement, + require scalar-zero points to use the canonical identity encoding or validate + every decoded non-identity point with a cheap precompile call. + +### Finding 2. Public proof repacker panics on malformed input + +Severity: P3. + +Relevant code: + +- `src/codegen/generator.rs:3021-3045` + +The old panicking public proof repacker has been replaced by +`SolidityGenerator::repack_proof`, which returns typed `RepackError` values for +wrong lengths and invalid compressed G1 bytes. + +Recommendation: + +- Keep production callers on `repack_proof` or `encode_calldata` and handle the + returned `Result`. + +## 2026-05-03 time-boxed manual audit: posted verifier review + +Scope: verifier as posted for manual review. + +This pass was not compiled or run against a native verifier. Treat it as a +manual review, not a proof of correctness. + +### Overall Assessment + +The design has several good hardening choices: exact ABI-layout checks, exact +proof length checks, canonical scalar checks for public instances and proof +evals, canonical BLS12-381 coordinate checks before transcript absorption, +dependency `code.length` + `codehash` pinning, and return-size checks on +precompiles. + +The highest-risk area is the IVC accumulator public-input decoding, especially +the branches that skip precompile validation when an accumulator scalar is +zero. This is exactly the kind of boundary where previous verifier audits have +found issues: missing proof-point/scalar validation, non-canonical encodings, +transcript deviations, and off-curve witnesses are recurring themes in +PLONK/Halo2 verifier audits. OpenZeppelin's Linea PLONK audit explicitly called +out missing validation of openings, public witness values, proof commitments, +and subgroup membership as verifier risk areas. Common Prefix's PLONK verifier +audit similarly flags canonical field representations and transcript/SRS +binding as important verifier concerns. Trail of Bits' Axiom Halo2 audit shows +how off-curve point witnesses can become proof-forgery hazards when later +arithmetic assumes valid curve points. + +### TA-1. Accumulator Points With Scalar Zero Bypass Curve/Subgroup Validation + +Severity: Medium/High, depending on circuit assumptions. + +In `load_acc_point`, accumulator coordinates are decoded from public instances +into `ACC_LHS_MPTR` and `ACC_RHS_MPTR`. The decoded point is range/canonical +encoding checked, but not independently checked to be on-curve or in the +correct subgroup. The code appears to rely on EIP-2537 precompiles to validate +points later. + +That reliance has an exception: + +```solidity +switch lhs_scalar +case 0 { + mstore(ACC_LHS_MPTR, 0) + ... +} +``` + +and similarly for the RHS path: + +```solidity +if and(iszero(acc_msm_len), iszero(rhs_kept_direct)) { + mstore(ACC_RHS_MPTR, 0) + ... +} +``` + +If `lhs_scalar == 0` or `rhs_scalar == 0`, a non-identity accumulator point can +be decoded from public inputs and then overwritten with the identity before any +precompile sees it. This means invalid/off-curve public accumulator coordinates +can be accepted when their scalar is zero. + +Why this matters: if the circuit expects the Solidity verifier to validate +public accumulator points, a malicious prover can potentially publish malformed +accumulator limbs while setting the corresponding scalar to zero. The pairing +equation no longer checks that malformed point. Even if this does not forge the +final KZG proof, it can accept an invalid public IVC accumulator statement. + +Recommendation: + +- Validate every non-identity decoded accumulator point before considering its + scalar. For example, immediately after `load_acc_point` succeeds, call a + validating precompile path with scalar `1` and ignore the result: + + ```yul + function validate_g1_point(mptr) { + let scratch := 0x100 + mcopy(scratch, mptr, 0x80) + mstore(add(scratch, 0x80), 1) + + if iszero(staticcall(g1msm_gas_cap(0xa0), 0x0c, scratch, 0xa0, scratch, 0x80)) { + revert(0, 0) + } + if iszero(eq(returndatasize(), 0x80)) { + revert(0, 0) + } + } + ``` + +- Then: + + ```yul + if and(success, iszero(lhs_is_id)) { + validate_g1_point(ACC_LHS_MPTR) + } + if and(success, iszero(rhs_is_id)) { + validate_g1_point(ACC_RHS_MPTR) + } + ``` + +- Add a circuit-side on-curve/subgroup constraint for accumulator public points + if the circuit currently emits only raw limbs. + +### TA-2. Transcript Must Be Proven To Bind Every Verifier-Side Fixed Input + +Severity: Medium. + +The verifier absorbs `vk_digest` rather than the full VK payload: + +```yul +buf_len := common_word(buf_len, mload(VK_DIGEST_MPTR)) +``` + +This is fine only if `vk_digest` is specified to commit to all fixed verifier +data that affects verification, including: + +- fixed commitments; +- permutation commitments; +- quotient VM constants and bytecode; +- SRS material such as `G1_BASE`, `G2_BASE`, and `NEG_S_G2_BASE`; +- accumulator schema and packing parameters; +- protocol version and transcript encoding version. + +The VK comments say `vk_digest` is `transcript_repr` of the CS, which is +ambiguous. If it is only a circuit/constraint-system digest and not a digest of +the full verifier key/runtime payload, then Fiat-Shamir challenges are not +explicitly bound to all verifier-side fixed data. + +The codehash pinning strongly mitigates this for a single deployed verifier, +but the transcript specification should still be exact. This is the same class +of issue as the Espresso PLONK verifier finding where the transcript did not +include all common preprocessed input/SRS material. + +Recommendation: + +- Define `vk_digest` as something like: + + ```text + vk_digest = H( + "midnight-halo2-bls12-381-v1", + vk_runtime_payload, + quotient_evaluator_runtime_hash, + proof_layout_id, + accumulator_layout_id, + transcript_encoding_id + ) + ``` + +- Or absorb `EXPECTED_VK_CODEHASH` and `EXPECTED_QUOTIENT_CODEHASH` directly + into the transcript before public instances. + +### TA-3. External Quotient Evaluator Is Pinned, But Not Transcript-Bound + +Severity: Low/Medium. + +`AUTHORIZED_QUOTIENT` is codehash-pinned, which is good. However, its output +directly determines: + +```yul +QUOTIENT_EVAL_MPTR +SELECTOR_ACC_MPTR +``` + +and the transcript never absorbs the quotient evaluator identity. If the +quotient evaluator is treated as part of the verifier key, this should be +included in the VK digest or absorbed separately. + +Recommendation: + +- Include `EXPECTED_QUOTIENT_CODEHASH`, `EXPECTED_QUOTIENT_LENGTH`, and a + quotient evaluator version/magic in the same digest used for Fiat-Shamir + domain separation. The returned `QUOTIENT_MAGIC` is a good runtime guard, but + it does not bind challenges to the evaluator. + +### TA-4. Gas Checkpoint Logs Make This Unsuitable As A Production Verifier + +Severity: Low/Medium. + +`verifyProof` can include gas checkpoint logging: + +```yul +function gas_checkpoint(id) { + log1(0, 0, or(shl(248, id), gas())) +} +``` + +This has three consequences: + +1. It increases gas materially. +2. It emits trace logs on every verification. +3. It makes the verifier unusable through `STATICCALL`, because `LOG1` is + state-changing. + +Many application contracts expect proof verifiers to be `view`-like, even if +the interface is not marked `view`. A gas-checkpoint artifact will fail in any +static context. + +Recommendation: + +- Feature-gate this at codegen time. Production builds should make + `gas_checkpoint` a no-op: + + ```yul + function gas_checkpoint(id) { + pop(id) + } + ``` + +- Keep the logging version only in a dedicated gas/trace artifact with a + different contract name and codehash. + +### TA-5. Dangerous Reliance On `assembly ("memory-safe")` With Absolute Memory Ownership + +Severity: Low/Medium. + +The main assembly block is marked `"memory-safe"` while it intentionally owns +the entire call-frame memory, writes to low memory, uses fixed absolute +pointers, and returns from assembly. + +Because the block is terminal, this may be practically safe, but the annotation +is fragile. It tells the Solidity optimizer that the assembly obeys Solidity's +memory-safety rules. If future edits add Solidity code after the block, or if +the compiler reasons across the block in an unexpected way, this becomes a +miscompilation risk. + +Recommendation: + +- Prefer removing `"memory-safe"` from the terminal verifier block unless there + is a compiler-specific proof that this pattern is accepted. +- Pin the exact compiler and EVM version. The Renegade audit's recommendation + to use fixed pragmas rather than floating `^0.8.x` is especially relevant for + generated verifier code. +- Use: + + ```solidity + pragma solidity 0.8.24; + ``` + + or the exact version tested and pinned in CI. + +### TA-6. Precompile Smoke Tests Are Too Weak For Deployment Confidence + +Severity: Low. + +The constructor checks identity inputs for G1ADD, G1MSM, and pairing. That +catches absent precompiles and gross return-size issues, but it does not catch: + +- coordinate endianness mismatches; +- non-identity arithmetic bugs; +- subgroup-check differences; +- invalid-point rejection behavior; +- target-chain gas-schedule differences. + +Recommendation: + +- Add deployment/CI tests against the target chain or fork that exercise: + - nontrivial `G1ADD(P, Q)`; + - `G1MSM([(P, a), (Q, b)])`; + - invalid G1/G2 encodings must fail; + - known-valid and known-invalid pairing equations; + - boundary scalars `0`, `1`, `r - 1`, `r`. + +### TA-7. Root-Of-Unity / Zero-Denominator Cases Revert Rather Than Being Specified + +Severity: Informational/Low. + +The Lagrange and PCS blocks intentionally batch-invert values like: + +```yul +x - omega^i +x^n - 1 +x3 - rotation_point +``` + +If a challenge lands on a denominator-zero case, the verifier reverts. This is +probably acceptable because the probability is negligible, but it should be +explicitly specified as "reject on exceptional Fiat-Shamir challenge." + +The Espresso audit had a high-severity PLONK verifier issue around incorrect +Lagrange/public-input behavior when `zeta` is a root of unity, so this edge case +deserves explicit tests even if the intended behavior here is rejection. + +Recommendation: + +- Add negative tests with a harness that overrides transcript challenges to + force: + - `x^n = 1`; + - `x = omega^i`; + - `x3 = x * omega^rotation`. +- Expected result: revert, not accept. + +### TA-8. Raw Verifier Integration Can Still Be Replayed/Misused By Application Contracts + +Severity: Integration risk. + +The verifier correctly says application contracts must bind the meaning of +public instances separately. That warning is important. This raw verifier +accepts any proof for the pinned circuit and supplied instances; it does not +enforce: + +- chain ID; +- application contract address; +- program ID; +- state root freshness; +- nullifier use; +- proof purpose; +- expected IVC output; +- caller authorization. + +Recommendation: + +- Application contracts should not expose a generic "verify and trust all + instances" flow. They should decode the public instances and explicitly check + every semantic field before acting on the proof. + +### Time-Boxed Audit Priorities Before Production + +1. Fix accumulator zero-scalar point validation. +2. Formally define and test `vk_digest` coverage. +3. Remove `gas_checkpoint` from production artifacts. +4. Pin compiler + EVM version; reconsider `"memory-safe"` on the terminal + block. +5. Run a differential test suite against the native Midnight/Halo2 verifier. +6. Add mutation tests that remove each range check, point check, codehash check, + and proof-length check; the tests should fail. + +A strong negative test suite should include malformed ABI offsets, short/long +proof bytes, non-canonical scalars, `x_hi`/`y_hi` high-bit pollution, `x = p`, +`y = p`, off-curve G1s, wrong-subgroup G1s if available, zero-scalar +accumulator malformed points, VK codehash mismatch, quotient codehash mismatch, +and forced zero-denominator transcript challenges. + +## 2026-05-06 audit addendum: Solidity/Yul verifier shape + +I found a few real issues / risky assumptions. I did **not** fully prove the +Halo2 algebra matches the Rust verifier; this is a manual security pass over +the Solidity/Yul verifier shape. + +### Highest-priority findings + +#### 1. **High/Critical on some chains: VK is only codehash-checked in the constructor** + +The verifier checks: + +```solidity +authorizedVk.code.length == EXPECTED_VK_LENGTH +authorizedVk.codehash == EXPECTED_VK_CODEHASH +``` + +only once, in the constructor. Later `verifyProof` does: + +```yul +extcodecopy(vk, VK_MPTR, 0x00, 0x4280) +``` + +with no fresh `extcodesize` / `extcodehash` check. + +On Ethereum-style chains with EIP-6780 semantics, `SELFDESTRUCT` generally no +longer deletes code except when called in the same transaction as creation, so +this is much less exploitable there. But on forks/L2s/alt-EVMs without +equivalent semantics, or in same-transaction edge cases, a metamorphic VK +address could be changed after verifier deployment. EIP-6780 explicitly says +same-transaction-created contracts keep old deletion behavior, and older +`CREATE2` redeploy patterns are not supported after the change. ([Ethereum +Improvement Proposals][1]) + +Impact if mutable: catastrophic. The VK payload contains the transcript VK +digest, quotient VM program/constants, fixed/permutation commitments, and G1/G2 +bases. A replaced VK could likely make the verifier accept attacker-controlled +proofs or brick all verification. + +Fix: + +```yul +if iszero(and( + eq(extcodesize(vk), EXPECTED_VK_LENGTH), + eq(extcodehash(vk), EXPECTED_VK_CODEHASH) +)) { revert(0, 0) } + +extcodecopy(vk, VK_MPTR, 0x00, EXPECTED_VK_LENGTH) +``` + +Or embed the VK payload directly in the verifier. + +#### 2. **High/Medium: accumulator decoder appears to accept non-canonical identity encoding** + +`load_acc_coord` maps encoded `p - 1` to coordinate zero: + +```yul +let was_p_minus_one := is_bls_p_minus_one(hi, lo) +if was_p_minus_one { + hi := 0 + lo := 0 +} +``` + +For the x-coordinate, `load_acc_coord(... allow_id = 1 ...)` detects the +explicit identity flag only if subtracting `base` produces `p - 1`. But if the +public input encodes both coordinates as `p - 1` **without** the x identity +flag, the decoder can still output EIP-2537 `(0,0)` while `is_id == false`. + +EIP-2537 defines `(0,0)` as the point-at-infinity encoding for G1/G2, and +requires subgroup checks in MSM/pairing precompiles. ([Ethereum Improvement +Proposals][2]) So the later G1MSM will treat this as identity. + +Impact depends on the circuit. If the circuit already enforces the exact +`AssignedForeignPoint` identity encoding, this is mostly public-input +malleability. If not, this can become a soundness issue for the public IVC +accumulator: the verifier may treat an accumulator point as identity even when +the circuit/public input did not mark it as identity. + +Fix: after decoding a non-identity accumulator point, reject decoded `(0,0)` +unless the canonical identity encoding was used. + +Conceptually: + +```yul +let decoded_zero := iszero(or(or(x_hi, x_lo), or(y_hi, y_lo))) +if and(decoded_zero, iszero(is_id)) { + ok := 0 +} +``` + +More strictly, require `is_acc_encoded_identity(src)` for any decoded infinity. + +#### 3. **Medium: production verifier still emits gas checkpoint logs** + +`gas_checkpoint` is active in `verifyProof`: + +```yul +function gas_checkpoint(id) { + log1(0, 0, or(shl(248, id), gas())) +} +``` + +This makes valid verification emit many `LOG1`s and prevents use via +`STATICCALL`. Many verifier integrations expect proof verification to be +`view`-like; this implementation will revert under static context because `LOG` +is a state-changing opcode. + +Impact: integration breakage and unnecessary gas/event pollution. Not a +proof-forgery bug. + +Fix: compile gas checkpoints only in a trace build, or guard/remove them in +production. + +#### 4. **Medium: strong reliance on exact EIP-2537 semantics and gas schedule** + +The verifier calls BLS12-381 precompiles at `0x0b`, `0x0c`, and `0x0f`, +matching EIP-2537's G1ADD, G1MSM, and pairing addresses. ([Ethereum Improvement +Proposals][2]) It also relies on field-element encoding rules, including +64-byte big-endian Fp elements with top 16 bytes zero and `< p`, and on +MSM/pairing subgroup checks. ([Ethereum Improvement Proposals][2]) + +The constructor smoke test checks only identity inputs. It does **not** prove +that the chain's precompiles reject malformed/non-subgroup points, implement +the same gas schedule, or handle the 78-term MSM used later. + +Impact: on a non-conforming fork, this can be either liveness failure or +soundness failure. + +Fix: document exact supported chains/forks, keep the constructor smoke test, +and add deployment/CI tests for malformed points, non-identity generator +arithmetic, pairing bilinearity, and the full-size MSM gas path. + +### Lower-severity issues / hardening + +#### 5. Accumulator validation happens very late + +Malformed accumulator public inputs are only decoded and G1MSM-validated after +the expensive transcript, quotient, identity, and PCS work. A relayer or +subsidized caller can be griefed with inputs that fail late. + +Fix: do accumulator limb packing checks and G1MSM point validation near the +start, then keep only the final pairing batch until the end. + +#### 6. The VK "data-only" runtime is still executable bytecode + +For this exact VK, byte 0 is `0x56` (`JUMP`) with an empty stack, so direct +calls should immediately fail. But the general "runtime contains no callable +code" claim is fragile: arbitrary data bytecode is still executable. A future +VK whose first bytes accidentally form a reachable `SELFDESTRUCT` path would be +dangerous on chains where deletion is possible. + +Fix: use a safe runtime wrapper or prefix with an unconditional `STOP`/`INVALID` +and adjust offsets/hash accordingly. + +#### 7. Quotient VM has trusted-program assumptions + +The VM has no stack-depth or pointer-range checks, and the comment lists opcode +`0x20 POW5` although the shown interpreter has no `case 0x20`. Because the +program is codehash-pinned, this is not directly user-exploitable, but it is a +codegen/liveness risk. + +Fix: add an offline decoder test that proves the pinned `q_program` uses only +implemented opcodes and never underflows the VM stack; ideally add a known-good +proof test in constructor-time or deployment CI. + +### What I would fix first + +1. Re-check `AUTHORIZED_VK` length/hash inside `verifyProof`. +2. Reject non-canonical accumulator infinity encodings. +3. Remove `gas_checkpoint` from production. +4. Add conformance tests for EIP-2537 precompile behavior and full-size MSM gas. +5. Add generator tests that decode the quotient VM bytecode and compare + Solidity outputs against the Rust verifier on valid and invalid proofs. + +[1]: https://eips.ethereum.org/EIPS/eip-6780 "EIP-6780: SELFDESTRUCT only in same transaction" +[2]: https://eips.ethereum.org/EIPS/eip-2537 "EIP-2537: Precompile for BLS12-381 curve operations" diff --git a/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md b/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md new file mode 100644 index 000000000..0986048bf --- /dev/null +++ b/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md @@ -0,0 +1,1419 @@ +# Solidity Verifier Codegen Audit + +**Repository:** `halo2-solidity-verifier-exp` +**Scope:** halo2 → midnight-proofs port of the on-chain KZG/BLS12-381 verifier +codegen and emitted Yul. +**Files reviewed (deep read):** + +- `src/codegen/util.rs`, `memory.rs`, `protocol.rs`, `transcript.rs`, + `generator.rs`, `pcs.rs`, `quotient/mod.rs`, `evaluator.rs` +- `templates/contracts/Halo2Verifier.sol`, `templates/partials/quotient_numerator/QuotientNumeratorBlock.yul` +- `docs/architecture/MEMORY_LAYOUT.md`, `docs/reference/QUOTIENT_NUMERATOR_EVALUATOR.md` + +The audit explicitly looked for the bug classes the requestor flagged +(EIP-2537 padding, transcript / hash-to-challenge mismatches, MSM index +errors, batch-invert zero handling, memory-region collisions, lookup / +permutation chunk boundaries, blinding rows, `omega_inv_to_l` exponent, +dummy-eval transcript handling, `proof_total()` ↔ calldata cursor +agreement, `num_committed_instances` boundary, panics/TODOs in the +emitted artifact, EIP-2537 precompile constants, padded-G1-loaded-as-2 +issues, etc.). Each individual class was instrumented by reading the +relevant emitter, the corresponding Yul, and the supporting offset +arithmetic. + +Bottom line: I did **not** find a clear, exploitable correctness bug in +the production verifier path. The codebase has clearly been hardened +since the original BN254 fork — every high-risk surface I checked +(transcript domain separation, EIP-2537 canonicality, proof-cursor +agreement, batch_invert zero check, KZG fused MSM, accumulator +randomization) lines up with the documented midnight-proofs +convention. The findings below are mostly **Suspicious / Hardening** +items where the code is correct today but the invariants are subtle +enough to be worth pinning down. + +--- + +## Current reconciliation status + +This file is the open-issues ledger for the assurance dossier in +[`docs/audit/CODEGEN_ASSURANCE_DOSSIER.md`](./CODEGEN_ASSURANCE_DOSSIER.md). +The older finding text is retained below for audit history; the table here is +the release-facing status snapshot as of 2026-05-11. + +| Finding | Status | Evidence | +| --- | --- | --- | +| M1 lookup helper/accumulator cursor grouping | Fixed | `Data::new` now derives section starts from `ProofCalldataLayout`; `proof_layout_preserves_lookup_helper_accumulator_grouping` locks the interleaved helper/accumulator order. | +| M2 ambiguous advice commitment order metadata | Fixed | `CommitmentRead::Advice` is category-only; column/query identity lives in eval and PCS plans. `ProtocolPlan::validate` and commitment group tests cover plan drift. | +| 2026-05-11 #1 lookup quotient identity count | Fixed | `lookup_chunks.iter().map(|chunks| chunks + 2).sum()` is used in planning and validation; `lookup_identity_source_handles_variable_chunk_counts` covers variable chunk counts. | +| 2026-05-11 #2 fixed eval count | Fixed | `proof_evaluation_counts().fixed` counts `EvalRead::Fixed` entries from the protocol plan instead of fixed columns. | +| 2026-05-11 #3 challenge phase remapping | Fixed | `ProtocolPlan::from_constraint_system` sizes phases by the max of advice and challenge phases; `plan_allows_challenge_phase_beyond_advice_phases` covers this case. | +| 2026-05-11 #4 packed32 operand widths | Fixed | `validate_packed_quotient_operand` enforces logical `u8` and `u16` bounds; `packed32_validator_rejects_logical_operand_width_corruption` covers corrupted packed operands. | +| 2026-05-11 #5 reserved memory writes | Fixed | Trace and helper templates avoid Solidity-reserved memory writes; `templates_do_not_write_solidity_reserved_memory_slots` checks `mstore(0,`, `mstore(0x00,`, and related reserved forms. | +| 2026-05-11 #6 external quotient return overlap | Fixed | External quotient output uses `QUOTIENT_RETURN_MPTR`; template validation checks output length and disjointness from the copied quotient frame. | +| 2026-05-11 #7 structured selector-run trace | Fixed | Selector-run grouping is disabled when trace is enabled, preserving per-identity trace events through direct quotient blocks. | +| 2026-05-11 #8 proof layout count reconstruction | Fixed | `ProofCalldataLayout::from_protocol` replays `protocol.proof.commitments` and panics on category drift; `proof_layout_rejects_commitment_order_drift` covers it. | +| 2026-05-11 #9 identity committed instance policy | Accepted restriction | `SolidityGenerator::SUPPORTED_COMMITTED_INSTANCE_COMMITMENT` exposes the identity policy and shape validation requires exactly one committed and one non-committed instance column. Generic committed-instance commitments remain out of scope. | +| 2026-05-11 #10 shape profiling undercount | Fixed | `emit_acc_leaf` records fallback VM ops for constant and short-memory accumulator opcodes. | + +Production-scope exclusions remain: generic Halo2 circuit support, generic +committed-instance commitments, application wrapper binding/replay policy, and +chains without the expected EIP-2537 semantics. + +--- + +## Critical — None identified + +I traced the highest-risk vectors end-to-end and could not find a +break: + +- **Transcript domain separation.** `templates/contracts/Halo2Verifier.sol` + (lines ~990–1100) absorbs `vk_digest`, the 128-byte zero + `committed_pi` identity, the BE `num_instances` length scalar, then + each BE instance, before any proof bytes. This matches the patched + `Hashable for G1Projective::to_input` in midnight-proofs + (the previous emitter used the 48-byte ZCash compressed encoding; + the new emitter is consistent with the patched native verifier). +- **EIP-2537 padded G1 (4 words).** Every commitment read goes + through `common_uncompressed_g1` (lines ~480–520): + `if shr(128, x_hi_word) { revert(0,0) }`, the BLS12-381 Fp range + check `(hi, lo) ≤ p − 1`, and a verbatim `calldatacopy` of 0x80 + bytes into the transcript. There is no path where a 2-word load + reaches a precompile. +- **Squeeze-to-Fr.** `squeeze_to` (~line 528) reseeds with the 32-byte + Keccak digest and samples `mod(h0, FR_MODULUS)` exactly once per + challenge. No truncation/length mistake (`buf_len` is reset to 32). +- **`scalar_inv` location.** `scalar_inv` (~line 340) intentionally + uses the dead transcript region just below `VK_MPTR` + (`p := sub(VK_MPTR, 0x100)`). The verifier never calls it before + transcript absorption is finished, and the comment correctly + documents that this avoids collisions with PCS scratch when the VK + payload shrinks. +- **`batch_invert` zero check.** Production `batch_invert` (~line 540) + rejects the input batch if the **product** is zero + (`if iszero(gp) { ret := 0; leave }`) and short-circuits the + singleton case via `if iszero(x) { ret := 0; leave }`. With the + current call sites (Lagrange denominators + the GWC dummy/Lagrange + basis Montgomery batch in `pcs.rs`) every input is provably + non-zero by Fiat–Shamir, so the product check is sufficient. +- **EIP-2537 precompile constants.** Constructor smoke test (line 192) + exercises `0x0b` G1ADD, `0x0c` G1MSM and `0x0f` PAIRING_CHECK at + deploy time and reverts on absent / size-mismatched returndata. +- **Public-accumulator pairing batch.** The `acc_pair_alpha` derivation + (lines ~1530–1610) keccaks the full + `domain || PAIRING_RHS || PAIRING_LHS || ACC_RHS || ACC_LHS` payload + *after* the MSM is fully constructed, defeats the trivial + multiplicative-cancellation attack, and falls back to `1` only when + the digest happens to be `0` (probability ≈ 2â»Â²âµâ¶). +- **Quotient VM dispatch.** `templates/partials/quotient_numerator/QuotientNumeratorBlock.yul` + dispatch covers `0x01..0x1e`, with `default { revert(0,0) }` on the + inner token switches (so out-of-range token indices fail closed). + The `q_y_inv` modexp uses a separate `q_inv_scratch = + program.stack_mptr` from `scalar_inv`’s transcript pad, eliminating + the previous large-VK collision risk. + +--- + +## High — None identified + +I audited the calldata cursor agreement, the EIP-2537 padded +serialization, the multi-prepare KZG (Block 5) MSM term count, the +linearization expansion of the quotient limbs and selectors, the +public-accumulator decoding and identity-flag fixup, and the dummy +eval/transcript handling. All pass the consistency checks I could +construct from the code alone. + +The two items I want to flag for follow-up review are below. + +--- + +## Medium + +### M1. `cd_byte` cursor groups lookup helpers/accumulators in a different physical order than the verifier reads them + +**File:** `src/codegen/util.rs:443–448` + +```rust +let mut cd_byte = proof_cptr_bytes; +cd_byte += G1_BYTES * meta.advice_indices.len(); +cd_byte += G1_BYTES * meta.num_lookups; // multiplicities +cd_byte += G1_BYTES * meta.num_permutation_zs; // perm Z +cd_byte += G1_BYTES * lookup_helper_total; // ALL helpers +cd_byte += G1_BYTES * meta.num_lookups; // ALL accumulators +cd_byte += G1_BYTES * meta.num_trashcans; +let quotient_limb_cd = cd_byte; +``` + +**File:** `templates/contracts/Halo2Verifier.sol:~1130–1150` + +```yul +{%- for chunks in lookup_chunks %} +// lookup {{ loop.index0 }}: {{ chunks }} helper(s) + 1 acc +for { let end := add(proof_cptr, ... ) } lt(proof_cptr, end) { } { + ... helpers ... +} +buf_len := common_uncompressed_g1(buf_len, proof_cptr) // accumulator +calldatacopy(lookup_z_walk, proof_cptr, 0x80) +proof_cptr := add(proof_cptr, 0x80) +{%- endfor %} +``` + +The on-chain reader interleaves `helpers + acc` per lookup. The +codegen-side `cd_byte` walk above sums "all helpers, then all +accumulators". The total byte count is identical +(`G1_BYTES * (Σchunks + num_lookups)`), and `cd_byte` is only used to +derive `quotient_limb_cd` / `eval_cd`, so the **current** code is +correct. + +The reason this is medium and not just suspicious: any future change +that exposes a per-lookup calldata pointer (say, to read an individual +helper commitment from calldata directly during quotient evaluation) +will compute the wrong offsets if it follows this cursor convention. +The cursor walk should mirror the actual on-chain interleaving — even +when only the running total is consumed today. + +**Suggested fix:** rewrite the helper/acc accumulator as a per-lookup +loop (mirroring `templates/contracts/Halo2Verifier.sol`) so the math is locally +obvious rather than relying on commutativity of the running sum. + +```rust +for &chunks in &meta.lookup_chunks { + cd_byte += G1_BYTES * chunks; // helpers for this lookup + cd_byte += G1_BYTES; // accumulator for this lookup +} +``` + +### M2. `ProofReadPlan::commitments` stores phase-sorted `column` indices but iterates in original-column order + +**File:** `src/codegen/protocol.rs:312–318` + +```rust +proof.commitments.extend( + advice_indices + .iter() + .copied() + .map(|column| CommitmentRead::Advice { column }), +); +``` + +`advice_indices[orig_col]` holds the **proof position** of the +original column after phase sorting, so the *value* placed in +`CommitmentRead::Advice { column }` is the proof index, but the +iteration order is the original column order, not the proof +order. Concretely with phases = `[1, 0]`: + +- `advice_indices = [1, 0]` +- `proof.commitments = [Advice{column=1}, Advice{column=0}]` + ↑ but proof slot 0 in calldata is the phase-0 column (orig col 1) + and proof slot 1 is phase-1 (orig col 0). + +So `proof.commitments[i]` does not satisfy "the column read at proof +slot i" *and* the `column` field's semantic (orig vs phase-sorted) +flips depending on viewpoint. Today this is benign because the only +consumer (`proof_total()` at line 536) just reads `commitments.len()`, +but any downstream code that iterates `proof.commitments` and +dereferences `column` will get either the wrong order or the wrong +identifier mapping. + +**Suggested fix (one of):** + +1. Iterate phases explicitly so the resulting vector is in physical + proof order: + ```rust + for phase in 0..num_phase { + for (orig_col, p) in cs.advice_column_phase().iter().enumerate() { + if *p as usize == phase { + proof.commitments.push(CommitmentRead::Advice { column: orig_col }); + } + } + } + ``` +2. Or change the `column` field's documented meaning to "phase-sorted + index" and add a doc-comment + unit test pinning the contract. + +--- + +## Suspicious / hardening + +### S1. `Lagrange & instance-evaluation` block depends on `num_neg_lagranges ≥ 1` + +**File:** `templates/contracts/Halo2Verifier.sol:~1295–1340` + +```yul +let mptr := X_N_MPTR +let mptr_end := add(mptr, mul(0x20, add(mload(NUM_INSTANCES_MPTR), + {{ num_neg_lagranges }}))) +if iszero(mload(NUM_INSTANCES_MPTR)) { + mptr_end := add(mptr_end, 0x20) +} +... +let l_blind := mload(add(X_N_MPTR, 0x20)) +let l_i_cptr := add(X_N_MPTR, 0x40) +for { let l_i_cptr_end := add(X_N_MPTR, {{ (num_neg_lagranges * 32)|hex() }}) } + lt(l_i_cptr, l_i_cptr_end) { l_i_cptr := add(l_i_cptr, 0x20) } { + l_blind := addmod(l_blind, mload(l_i_cptr), r) +} +``` + +Reading slot `1` (`l_blind` seed) and slot `num_neg_lagranges` (`l_0`) +both assume the loop produced at least 2 Lagrange denominators. With +the standard halo2 construction `num_neg_lagranges = blinding_factors ++ 1 ≥ 2`, so this holds in practice. But the protocol-side +`rotation_last = -(blinding_factors + 1)` is computed with no lower +bound check. + +**Suggested fix:** add `assert!(meta.rotation_last.unsigned_abs() >= +1, ...)` (or `>= 2` if blinding is required) early in +`ProtocolPlan::from_constraint_system` so the codegen fails fast on +edge-case constraint systems. Equivalently, render the template only +for `num_neg_lagranges >= 2`. + +### S2. Linearization formula uses `x_split = x^(n-1)` (not the more common `x^n`) + +**File:** `templates/contracts/Halo2Verifier.sol:~1420–1440`, +`docs/reference/QUOTIENT_NUMERATOR_EVALUATOR.md:~452` + +```yul +let x_pow_2i := x +let x_pow_2i_minus1 := 1 +for { let idx := 0 } lt(idx, k) { idx := add(idx, 1) } { + x_pow_2i_minus1 := mulmod( + mulmod(x_pow_2i_minus1, x_pow_2i_minus1, r), x, r) + x_pow_2i := mulmod(x_pow_2i, x_pow_2i, r) +} +let x_split := x_pow_2i_minus1 // = x^(n-1) +let one_minus_x_n := addmod(1, sub(r, x_pow_2i), r) +``` + +That arithmetic is *correct* given the documented midnight-proofs +convention `linear_com = (1 − x^n) · Σ_i x^(i(n−1)) · Q_i`, but it +diverges from the upstream halo2 PSE convention `Σ_i x^(in) · Q_i`, +which is the formula most reviewers will check first. If an +auditor compares the limb scalar against a halo2-PSE reference +implementation they will see a mismatch that is not, in fact, a bug. + +**Suggested fix:** add a single-line comment in +`generate_pcs_computations` (Block 5) pointing to +`midfall/proofs/src/poly/kzg/mod.rs` so the convention is +self-documenting: + +```yul +// midnight-proofs splits h(x) as Σ_i x^(i*(n-1)) * h_i(x) (NOT x^(i*n)). +// Matches multiopen.rs::compute_linearization_commitment. +``` + +### S3. `compute_dummy_queries` panics on duplicate `(comm, rotation)` pairs + +**File:** `src/codegen/pcs.rs:194–201` + +```rust +Some(_) => { + panic!( + "duplicate (commitment, rotation) query at index {i}: \ + compute_dummy_queries cannot run on a non-deduplicated \ + query list" + ); +} +``` + +This is a programmer-side invariant; it is not reachable on a +well-formed PCS query schedule. But it is an unconditional panic +inside the generator, so a future bug elsewhere (e.g. duplicate +permutation queries due to a chunk-len off-by-one) would manifest as a +generator panic at codegen time rather than a structured error. + +**Suggested fix:** convert the panic into a `debug_assert!` plus a +typed `Err(...)` return, threaded through `try_new`. Same for the +`unreachable!("proof_cptr must be a literal byte offset")` in +`util.rs:439`. + +### S4. `f_eval` / `v` Horner direction relies on undocumented prover convention + +**File:** `src/codegen/pcs.rs:~1100–1230` (Block 4), +`src/codegen/pcs.rs:~1290–1320` (Block 5) + +The Block 4 reverse-Horner accumulator emits + +```text +f_eval = Σ_{s=0..n_sets-1} Ï€_s · x2^s +``` + +and Block 5 forward-Horner emits + +```text +v = Σ_{s=0..n_sets-1} q_evals[s] · x4^s + x4^{n_sets} · f_eval +``` + +i.e. `Ï€_0` and `q_evals[0]` get the constant coefficient `1`. This is +the midnight-proofs `multi_prepare` convention. There is no inline +comment binding the direction to a Rust source line; a reader following +the upstream halo2 PSE GWC implementation (which folds with the highest +power on the highest-index set) will see a sign/order mismatch that is +again not a real bug. + +**Suggested fix:** annotate Block 4 with the Rust source-of-truth and +add a unit test that pins `f_eval` for a 2-set, 3-set fixture against +a known reference vector. + +### S5. `g1msm_gas_cap` switch caps at `k = 128`; large MSMs fall through to the default branch + +**File:** `templates/contracts/Halo2Verifier.sol:~700–860` + +```yul +case 128 { discount := 519 } +// EIP-2537 G1MSM gas: k * discount[k] * 12000 / 1000. +cap := add(50000, div(mul(mul(k, discount), 12000), 1000)) +``` + +If `k > 128`, `discount` keeps the initial value `519`. The downstream +formula stays well-defined and the cap is just a *gas* cap (a too-low +cap would revert the call, not corrupt state). Today the only +≥128-term call site is the fused final MSM, whose term count is +bounded by `final_msm_shape(...)`. If a future circuit ever drives +`final_msm_terms` over 128, the cap will silently use the asymptotic +discount `519` rather than the precise table value, which can +under-estimate gas and revert valid proofs. + +**Suggested fix:** either extend the table up to the realistic upper +bound, or add a debug assertion in `final_msm_shape` that the term +count is `<= 128` (with a clear panic message when it's exceeded), so +the contract is rebuilt with a larger table before deployment. + +### S6. `acc_pair_alpha` zero-fallback uses `1` rather than re-hashing + +**File:** `templates/contracts/Halo2Verifier.sol:~1545–1555` + +```yul +let acc_pair_alpha := mod(keccak256(batch_ptr, 0x220), r) +if iszero(acc_pair_alpha) { acc_pair_alpha := 1 } +``` + +If the keccak digest is exactly `0 mod r`, the prover knows alpha +in advance and could craft pairing inputs that satisfy the batched +equation while individually failing. The probability is 2â»Â²âµâ¶, and +the security argument is fine; an auditor will flag this as a +deterministic-low-entropy-fallback hardening point. + +**Suggested fix:** in the iszero branch, re-hash with a domain +prefix (`keccak256("acc-batch-alpha-fallback" || alpha_seed)`) until +non-zero, or reject the proof. The cost is a single extra keccak in a +2â»Â²âµâ¶ branch, so always-rehash is cheap. + +### S7. Proof repacker returns typed errors + +**Status:** fixed. + +`SolidityGenerator::repack_proof` now returns +`Result, RepackError>` with length and compressed-G1 offset details. +`SolidityGenerator::encode_calldata` wraps repacking and also checks the +public-instance vector length before ABI encoding. + +--- + +## Things I checked and did NOT find an issue with + +This is the deliberate "no bug here" list, recorded so the next +auditor can skip the hot paths I already burned time on: + +- **`omega_inv_to_l = ω^{rotation_last}`** (`generator.rs:466`). + `rotation_last = -(blinding_factors + 1)`. The Lagrange seed loop + walks `ω^{rot_last}, ω^{rot_last+1}, ...`, so slot 0 holds + `L_{rot_last}(x) = l_last`, slot `num_neg_lagranges` holds `L_0(x)`. + Aligned with halo2 PSE. +- **Advice memory layout vs phase reordering** + (`util.rs:514`, `Halo2Verifier.sol:~1050`). + `advice_comms[orig] = comms_base + 4·advice_indices[orig]` and the + Yul reader stores phase 0 advice 0 at `comms_base`, phase 0 advice 1 + at `comms_base + 4`, etc. The mapping is consistent in both + directions for any phase permutation. +- **`Data::new` calldata cursor walk** + (`util.rs:439–520`). Total byte count agrees with + `transcript_buffer_words_bound` (`generator.rs:2965–3022`). +- **`construct_intermediate_sets` deduplication** + (`pcs.rs:~270–360`). Sets are deduplicated by the underlying + `EcPoint` memory pointer, so the dummy-query logic that depends on + pointer equality is well-defined. +- **EVM identity convention.** `G1_IDENTITY_MPTR` is reserved as a + 4-word region, never written. EIP-2537 (0,0,0,0) is the canonical + point-at-infinity. The "committed instances all alias + `G1_IDENTITY_MPTR`" pattern in `util.rs:498` matches the patched + `committed_pi = G1::identity()` in midnight-proofs. +- **Scratch lifetimes.** `MemoryArena` (`memory.rs:~200–500`) tracks + permanent vs phase-scoped scratch and asserts non-overlap in + `validate()`. The PCS-fixed window + (`rot_points`, `x1_powers`, `q_eval_set`, ...) is fixed-offset + inside the `theta`-relative slot 52+ band, with capacity sized + by `final_msm_shape`. No collision between batch-invert scratch and + PCS scratch; `BATCH_INV_SCRATCH_MPTR` is allocated inside the + scratch allocator rooted at `selector_acc_mptr`. +- **`num_committed_instances` branching.** `committed_instance_evals` + is filtered by `q.column < nb_committed_instances` + (`protocol.rs:340–345`), and the eval read pointer is the same + `eval_cptr` cursor used everywhere else. No double-counting. +- **`proof_total()` agreement.** + `protocol.rs:535` returns `commitments.len() + evals.len()`, used + only for sanity. The actual byte-level proof length is + `transcript_buffer_words_bound` × on-chain reads, and matches + `repack_proof`'s output length. + +--- + +## Recommended next steps + +1. Land **M1** and **M2** with unit tests pinning the calldata + ordering (M1) and the `column` semantics (M2). Both are mechanical + refactors. +2. Add inline "Rust source-of-truth" comments for the items in S2 and + S4 to make future auditors faster. +3. Convert the panics in S3 / S7 to typed errors so a generator-time + bug surfaces as a structured failure rather than a panic in CI. +4. Extend the `g1msm_gas_cap` table beyond `k = 128` (or assert the + bound) before any circuit pushes the fused final MSM past that + width (S5). +5. Optional: re-hash on the `acc_pair_alpha == 0` branch (S6); the + probability is negligible, but the cost of doing it right is a + single keccak. + +No production-blocking finding. The verifier should be safe to deploy +at the current revision, modulo the engineering hygiene items above. + +--- + +## 2026-05-11 additional audit findings + +I found several real issues worth fixing. + +### High-confidence bugs + +1. **Lookup quotient identity count is wrong for chunked lookups** + +`ProtocolPlan::from_constraint_system` uses: + +```rust +let lookup_identity_count = num_lookups * 3; +``` + +But `Evaluator::lookup_computations()` emits: + +```text +boundary + one helper per chunk + accumulator += 2 + lookup_chunks[lookup] +``` + +So any lookup with more than one helper chunk will make this assertion fail: + +```rust +assert_eq!(lookup.len(), meta.protocol.quotient.lookup) +``` + +Fix: + +```rust +let lookup_identity_count: usize = + lookup_chunks.iter().map(|chunks| chunks + 2).sum(); +``` + +Also fix lookup metadata currently using: + +```rust +let lookup_index = identity_index / 3; +``` + +That is wrong for variable chunk counts. + +--- + +2. **`proof_evaluation_counts().fixed` counts columns, not fixed eval queries** + +This is wrong: + +```rust +fixed: meta.num_fixeds - meta.num_simple_selectors, +``` + +Proof evals are query-based, not column-count-based. A fixed column can be queried at multiple rotations, or not queried at all. + +Use the protocol plan instead: + +```rust +fixed: meta.protocol.proof.evals.iter() + .filter(|e| matches!(e, EvalRead::Fixed(_))) + .count(), +``` + +Otherwise the public diagnostic API can panic on valid circuits because: + +```rust +counts.proof_total() == meta.num_evals +``` + +will fail. + +--- + +3. **Challenge phase remapping can panic if challenges use a phase beyond advice phases** + +`ProtocolPlan::from_constraint_system` computes: + +```rust +let num_phase = *cs.advice_column_phase().iter().max().unwrap_or(&0) as usize + 1; +``` + +Then it uses that same `num_phase` for `cs.challenge_phase()`. If a challenge phase exceeds the max advice phase, indexing can go out of bounds. + +Safer: + +```rust +let max_advice_phase = cs.advice_column_phase().iter().copied().max().unwrap_or(0); +let max_challenge_phase = cs.challenge_phase().iter().copied().max().unwrap_or(0); +let num_phase = max_advice_phase.max(max_challenge_phase) as usize + 1; +``` + +--- + +4. **Packed32 VM validator does not enforce operand widths** + +`decode_packed_quotient_instruction()` validates opcode support, but many operands are allowed to contain arbitrary 24-bit values even when the logical operand is `u8` or `u16`. + +Examples that should be rejected: + +```rust +Q_OP_PUSH_CONST_U8 // arg must be <= u8::MAX +Q_OP_ADD_CONST_U8 // arg must be <= u8::MAX +Q_OP_MUL_CONST_U8 // arg must be <= u8::MAX +Q_OP_PUSH_CONST // arg must be <= u16::MAX +Q_OP_ADD_CONST // arg must be <= u16::MAX +Q_OP_MUL_CONST // arg must be <= u16::MAX +Q_OP_PUSH_MEM_U16 // arg must be <= u16::MAX +Q_OP_ADD_MUL_MEM_MEM_CONST_U8 // scalar arg must be <= u8::MAX +``` + +The packer emits valid values, but the "safety validator" claims to reject malformed finalized programs. Right now it misses these packed operand-width corruptions. + +--- + +5. **Trace-only paths still write to memory slot `0`** + +You have tests/comments saying templates should not write Solidity-reserved memory, but trace failure paths do: + +```solidity +mstore(0, 34) +revert(0, 0x20) +``` + +and generated PCS trace code emits: + +```rust +"mstore(0, {}) revert(0, {WORD_BYTES:#x})" +``` + +Your string test only catches `"mstore(0x00,"`, not `"mstore(0,"`. + +Use `RETURN_MPTR` or `TRACE_U256_MPTR` instead. + +--- + +6. **External quotient return buffer is not modeled in the memory planner** + +Main verifier writes external quotient output to: + +```solidity +let q_out := SELECTOR_ACC_MPTR +... +staticcall(..., q_out, qext.output_len) +``` + +But `selector_accumulators` is registered with length: + +```rust +selector_len = num_simple_selectors * WORD_BYTES +``` + +while the output length is: + +```rust +2 * WORD_BYTES + selector_len +``` + +So the first two words of output intentionally overlap the following quotient temp/state area. This may be safe temporally, but the memory planner does not model it. Add a phase-scoped region for the external quotient output or use the low-memory `QUOTIENT_RETURN_BUFFER_START`. + +--- + +7. **Structured selector-run trace drops per-identity trace events** + +In structured-loop mode, grouped selector runs go through: + +```rust +selector_run_quotient_block(...) +``` + +but that path does not take `trace` and does not emit `push_quotient_trace` per identity. Trace builds with `CodegenConfig::quotient_structured_loops = true` can miss quotient identity trace IDs. + +Fix by disabling selector-run grouping under trace, or by emitting trace events inside the grouped loop. + +--- + +### Design / hardening issues + +8. **Proof layout still recomputes commitment order from counts** + +`ProofCalldataLayout::from_protocol()` receives `ProtocolPlan`, but it does not actually replay `protocol.proof.commitments`; it reconstructs the order from counts. If protocol order changes later, layout can silently drift. + +Better: build sections by walking `protocol.proof.commitments`, then validate category grouping. + +--- + +9. **Committed-instance commitment is hard-coded to identity** + +`Data::new()` does: + +```rust +let committed_instance_comms = + (0..meta.num_committed_instances) + .map(|_| EcPoint::new(Ptr::memory("G1_IDENTITY_MPTR"))) +``` + +and the Solidity transcript always absorbs identity committed_pi. That is fine for the zk_stdlib identity-commitment shape, but it is not a general "one committed instance column" verifier. This should be surfaced as an explicit generator restriction/API name, not only comments. + +--- + +10. **Shape profiling undercounts fallback VM ops** + +`emit_acc_leaf()` emits accumulator ops but does not call: + +```rust +record_fallback_vm_op() +``` + +So `fallback_vm_ops` is misleading when limb profiling is enabled. Not a correctness bug, but it weakens tuning data. + +--- + +## Additional review findings — 2026-05-12 + +The findings below were added after the reconciliation snapshot above. They are +review notes until each item is fixed with code/test evidence or explicitly +excluded from production scope. + +### Critical / High + +#### 1. Possible unbound evaluation in `q_eval_set[0]` + +In PCS sub-block 3, `q_eval_set[0]` folds 43 evaluations: + +```solidity +let q_eval_set_0 := mload(0x8a00) +... +for { let i := 1 } lt(i, 0x2b) { i := add(i, 1) } { ... } +``` + +This includes the `x1^1 * mload(0x8500)` term. However, in the final MSM +construction, the scalar sequence jumps from scalar `1` to `x1^2`: + +```solidity +mcopy(0xa300, 0x98c0, 0x80) +mstore(0xa380, 1) + +mcopy(0xa3a0, 0x9940, 0x80) +mstore(0xa420, mload(add(X1_POWERS_MPTR, 0x40))) // x1^2 +``` + +There is no point term with scalar `x1^1`. + +If `mload(0x8500)` is not intentionally the evaluation of a committed identity +polynomial, then this is a soundness bug: the prover can choose that evaluation +to satisfy quotient/permutation identities without it being bound by the KZG +opening check. + +**Recommendation:** Confirm what polynomial `0x8500` represents. If it is the +transcript-absorbed `committed_pi = identity`, document this explicitly and add +a differential test that mutating `0x8500` causes verification failure. If it +is not identity-committed, add the missing MSM term with scalar `x1`. + +#### 2. Public accumulator identity may collapse the recursive/IVC guarantee + +`load_acc_point` accepts the exact encoded identity for both accumulator points, +and the final pairing batch accepts a trivially true accumulator equation if: + +```solidity +ACC_LHS = identity +ACC_RHS = identity +``` + +This may be valid for an "empty accumulator" case. But if this verifier is +intended to enforce that an IVC accumulator actually carries a prior proof +state, then accepting both identity points lets the caller bypass the recursive +accumulator check. + +**Recommendation:** If the protocol requires a non-empty accumulator, reject +`(ACC_LHS, ACC_RHS) = (O, O)` or bind an explicit public input/state flag +proving that the empty accumulator is allowed. + +### Medium + +#### 3. Proof G1 validation relies entirely on later EIP-2537 use + +`common_uncompressed_g1` only checks that coordinates are canonical Fp +encodings. It does not check the curve equation or subgroup. The comments rely +on every absorbed proof commitment later being consumed by G1MSM or pairing. + +That is acceptable only if every deployment target implements EIP-2537 exactly. +EIP-2537 requires Fp elements to be 64-byte big-endian values with top 16 bytes +zero and `< p`, and states that MSMs and pairings must perform subgroup checks. +([Ethereum Improvement Proposals][1]) + +This contract's smoke test only uses identity inputs. It does not test +non-identity curve arithmetic, subgroup rejection, or invalid-point rejection. + +**Recommendation:** Add deployment or CI conformance tests using invalid G1/G2 +points and non-subgroup points. Consider explicitly validating proof +commitments with scalar-1 G1MSM when the verifier is deployed to +non-mainnet/custom EVMs. + +**Gas-cost note:** For the current Moonlight dump this strict runtime check +would be material but not catastrophic. A single scalar-1 G1MSM over the 32 +proof commitments costs about `240,768` precompile gas under the EIP-2537 +discount table. Including `f_com` and `pi` as well makes it 34 G1 points, about +`254,184` precompile gas before memory/call overhead, so roughly `~260k` total. +Checking each point with its own scalar-1 G1MSM would be worse, about `408k` +precompile gas for 34 points. A cheap G1ADD-based check would only be about +`12.8k` for 34 points, but it does not provide the subgroup validation this +finding is concerned with. + +#### 4. Debug gas checkpoints make verification non-static + +`gas_checkpoint()` emits `LOG1` throughout verification: + +```solidity +function gas_checkpoint(id) { + log1(0, 0, or(shl(248, id), gas())) +} +``` + +This means `verifyProof` cannot be called through `STATICCALL`. Many verifier +integrations assume verification is side-effect-free and call verifiers +statically. + +**Recommendation:** Remove gas checkpoints from production builds, or gate them +behind a separate debug build. + +#### 5. Accumulator/KZG pairing batching needs a written soundness argument + +The verifier batches two pairing equations with: + +```solidity +alpha = H(kzg_rhs, kzg_lhs, acc_rhs, acc_lhs) +``` + +and checks: + +```text +e(kzg_rhs + alpha * acc_rhs, G2) +* +e(kzg_lhs + alpha * acc_lhs, -sG2) += 1 +``` + +This is probably sound in the random-oracle model, but both equations' G1 +inputs are prover-influenced. The contract should include or reference a proof +that this Fiat-Shamir batching cannot be manipulated through fixed-point +selection of accumulator points. + +**Recommendation:** Document the batching lemma and add negative tests where +one equation is valid and the other is invalid. + +### Low / Informational + +#### 6. `q_program` interpreter lacks defensive invariants + +The quotient VM does not check final stack state, stack bounds, or that operands +stay inside expected memory regions. The program is VK-pinned, so this is not +attacker-controlled at runtime, but generator bugs become verifier soundness +bugs. + +**Recommendation:** In debug/test builds, assert: + +```text +q_pc == q_end +q_has_top == 0 +q_sp == base_stack_pointer +all memory operands are in approved ranges +``` + +#### 7. Raw verifier does not bind application semantics + +The verifier proves only that a proof verifies under the pinned VK and supplied +public instances. It does not bind state roots, program IDs, chain IDs, +nullifier domains, or application authorization. + +**Recommendation:** Wrapping contracts must domain-separate and validate all +public instances before accepting `verifyProof()`. + +#### 8. Invalid proofs revert instead of returning `false` + +This is documented, but it is an integration hazard. Any wrapper must use +`try/catch` or let invalid proofs revert the whole transaction intentionally. + +--- + +## Spec-level review findings - 2026-05-12 + +This review found several spec-level bugs and underspecified areas. The most +important ones are below. + +EIP-2537 note: the BLS12-381 padded G1/G2 encodings broadly match EIP-2537's +128-byte G1 and 256-byte G2 point encoding, and the addresses `0x0b`, `0x0c`, +and `0x0f` match G1ADD, G1MSM, and pairing. But EIP-2537 also says +variable-length operations such as MSM/pairing must error on empty input, and it +explicitly does **not** subgroup-check G1ADD, while MSMs and pairings must +subgroup-check. Those details affect several bugs below. ([Ethereum Improvement +Proposals][1]) + +### Highest-impact issues + +#### 1. Quotient VM persistent state aliases the quotient stack + +In Section 10.5, quotient VM state is placed at: + +```text +B = T + W*c +eval_numer_mptr = B +trace_id_mptr = B + W +selector_power_mptr = B + 2W +``` + +But in Section 9.5, `quotientTmp` is allocated with length +`C.quotientCseTemps * W`, and then `quotientStack` is allocated immediately +afterward. That means the persistent state begins exactly where the stack +begins, unless `C.quotientStackWords` is secretly meant to include the state +area. + +Impact: operand stack writes can overwrite `eval_numer`, `trace_id`, or the +selector-power table. Different implementers may allocate either "stack only" or +"state plus stack," producing different verifiers. + +Fix: define a separate `quotient_state` region: + +```text +quotientStateLen = (2 + selectorPowerWords) * W +quotientState = AllocAfter(... after quotient temps ...) +quotientStack = AllocAfter(... after quotient state ...) +``` + +Or explicitly define `C.quotientStackWords` as including persistent state, +operand stack, and native callback scratch. + +#### 2. Scalar-inversion scratch can be allocated inside Solidity-reserved memory + +Section 9.5 defines: + +```text +scalarInvScratch = AllocPhaseScratch( + scalar_inv_scratch, + max(vkStart - 0x100, 0), + 0xc0, + ScalarInv +) +``` + +But Section 9.1 says any nonzero planned region must start at `>= 0x80`. If +`vkMptr < 0x180`, then `max(vkMptr - 0x100, 0)` is below `0x80`, violating V3. +For example, `vkMptr = 0x80` gives scratch start `0x00`. + +Impact: the planner can deterministically produce an invalid memory map for +plausible VK starts. + +Fix: either require `vkMptr >= 0x180`, or change the allocation formula and +separately prove it cannot overlap the permanent VK region: + +```text +scalarInvStart = max(vkMptr - 0x100, 0x80) +require scalarInvStart + 0xc0 <= vkMptr +``` + +#### 3. Compact quotient program decoding needs a byte length, but the VK layout does not store one + +Section 5.3 says decoding is parameterized by an explicit unpadded byte length +`n`. Section 5.4 allows reserving more program words than are used, leaving +unused reserved words as zero. + +But the VK header and section layout do not store `n`. + +Impact: if a verifier decodes all reserved program words, the trailing zero +bytes become opcode `0x00`, which is invalid. If a verifier trims trailing +zeros, valid programs ending in zero bytes become ambiguous. Clean-room +implementations can diverge. + +Fix: add `quotient_program_byte_len` to the VK header, or define it as a +generated verifier constant that is part of conformance and pinning. Do not +infer it from section length or trailing zeros. + +#### 4. Accumulator validation is not specified + +The spec defines accumulator tail sizes, limb width, memory regions, and a +pairing-batch domain tag, but it does not define the actual accumulator +validation equations. + +Missing items include: + +- limb packing order and endianness inside public-input scalar words; +- limb range checks; +- reconstruction modulo the BLS12-381 base field; +- point-on-curve and subgroup checks; +- semantics of the "fixed-base scalar tail"; +- the MSM/pairing equations being checked; +- how the `pairing-batch-acc-kzg` domain tag is used. + +Impact: two conforming implementations could accept different accumulator +tails. Worse, an implementation could validate only the shape and not the +accumulator relation. + +Fix: add a normative accumulator-validation section with exact public input +parsing, reconstructed points/scalars, curve checks, batching challenge +derivation, MSM construction, and pairing equation. + +#### 5. `x_split` is undefined in the quotient commitment contribution + +Section 10.1 says the quotient commitment side contributes: + +```text +(1 - x^n) * sum_i x_split^i Q_i +``` + +But `x_split` is never defined. + +Impact: this is a soundness-critical scalar. Implementers may use `x`, `x^n`, a +truncated challenge, or a Halo2-specific split challenge. Only one can be +correct. + +Fix: define the exact split power. For example, if intended: + +```text +x_split = x^n +``` + +then say: + +```text +(1 - x^n) * sum_i (x^n)^i Q_i +``` + +Also define how this interacts with the "outer single-H" feature. + +#### 6. PCS query order and proof-evaluation read order differ, but no mapping is defined + +Main proof evaluations are read in this order: + +1. committed-instance evaluations; +2. advice evaluations; +3. fixed evaluations; +4. etc. + +PCS queries are ordered differently: + +1. advice queries; +2. committed-instance queries; +3. permutation product queries; +4. etc. + +The spec only says that the PCS query count must equal the main +proof-evaluation count before appending the synthetic linearization query. + +Impact: count equality is not enough. Each PCS query needs a precise pointer to +the claimed evaluation scalar read earlier. Otherwise an implementation could +pair an advice commitment with a committed-instance evaluation, or vice versa. + +Fix: define an explicit query-to-evaluation-slot map. For every PCS query triple +`(C, rho, e)`, specify exactly which evaluation-read entry supplies `e`. + +#### 7. Dummy evaluation count is ambiguous + +Section 6 defines the main proof-evaluation count as the list length before +dummy PCS evaluations are appended. Section 7 takes an evaluation scalar count +`E` for the proof layout. Section 8 says the transcript absorbs "all main +evaluation scalars, including dummy evaluation scalars when enabled." + +Impact: `E` can mean either raw main evaluations or raw plus dummy evaluations. +That changes proof length, transcript state, and the PCS query schedule. + +Fix: introduce separate names: + +```text +E_raw = non-dummy proof evaluations +E_dummy = dummy evaluations added by fewer-point-sets planning +E_total = E_raw + E_dummy +``` + +Then state: + +```text +proof layout uses E_total +transcript absorbs E_total +protocol invariant before dummy uses E_raw +PCS query list after dummy uses E_total +``` + +#### 8. Empty MSMs are possible but EIP-2537 rejects empty variable-length inputs + +The spec says identity commitments are omitted from MSM input but still +contribute to scalar equations. If a point set contains only identity +commitments, the commitment-side MSM for that set has zero terms. Similarly, +accumulator or final MSM shapes may be zero in some feature combinations. + +EIP-2537 requires variable-length operations such as MSM and pairing to error on +empty input. ([Ethereum Improvement Proposals][1]) + +Impact: a verifier that blindly calls G1MSM with zero terms will revert on +proofs that the mathematical spec may intend to handle. + +Fix: define zero-term MSM behavior in the generated verifier: + +```text +if msm_terms == 0: + result = G1 identity +else: + call G1MSM precompile +``` + +Also add a validation rule requiring every generated precompile call to have a +nonzero input length unless the precompile ABI allows empty input. + +#### 9. The spec overstates subgroup validation for G1ADD paths + +Section 8 says every absorbed proof G1 must later be consumed by an EIP-2537 path +that validates curve and subgroup membership. But EIP-2537's G1ADD precompile +does not subgroup-check; MSMs and pairings do. ([Ethereum Improvement +Proposals][1]) + +Impact: if an implementation routes an untrusted proof point through G1ADD and +treats success as subgroup validation, the spec's stated invariant is false. + +Fix: say specifically: + +```text +Every untrusted proof G1 must be passed to G1MSM or pairing, or otherwise +explicitly subgroup-checked, before its value can affect acceptance. G1ADD +success is not a subgroup check. +``` + +### Medium-impact issues and conformance hazards + +#### 10. `vk_digest` is not defined + +Header word 0 is `vk_digest`, and the transcript begins by absorbing it. But the +spec never defines how to compute it. + +Impact: proof compatibility depends on this digest. Two clean-room +implementations can produce different transcript challenges for the same VK. + +Fix: define the exact VK digest algorithm: serialization order, +inclusion/exclusion of quotient sections, fixed/permutation commitments, KZG +parameters, domain separators, endianness, and hash/reduction rules. + +#### 11. "Truncated challenges" are not specified + +Section 8 says: + +```text +If truncated challenges are enabled, stored powers of x1 and x4 are truncated +as in the Midnight verifier... +x3 is truncated immediately after squeezing. +``` + +This is not a clean-room specification. + +Impact: truncation changes the accepted proof language. "As in Midnight" is an +external implementation dependency. + +Fix: specify exactly: + +- number of bits retained; +- whether truncation happens before or after reduction mod `r`; +- whether powers are computed from truncated or full values; +- whether transcript absorption uses truncated or full challenge words; +- how `x3` truncation is encoded. + +#### 12. `fold_selector` encoding silently limits selector count and selector gaps + +Opcode `0x0b fold_selector` uses a 24-bit operand: + +```text +s = w >> 16 // 8 bits +g = w mod 2^16 // 16 bits +``` + +So it supports at most 256 selector buckets and selector gaps below 65536. + +Impact: large circuits can become unencodable or, worse, incorrectly encoded if +the generator truncates. + +Fix: add static validation: + +```text +numSimpleSelectors <= 256 +every selector gap <= 65535 +``` + +Or add a wider selector-fold opcode. + +#### 13. `c_perm = d - 2` can be zero or negative + +Section 6 defines: + +```text +c_perm = d - 2 +z_perm = ceildiv(|P|, c_perm) +``` + +If `d <= 2` and there are permutation columns, this divides by zero or by a +negative number. + +Impact: undefined generator behavior. + +Fix: require: + +```text +if |P| > 0 then d >= 3 +``` + +Probably reject `d < 3` globally unless there is a proven reason to allow it. + +#### 14. `omega_inv_to_l` / `rotationLast` notation is inconsistent + +Header word 6 is: + +```text +omega_inv_to_l = omega^{-L}, where L = |rho_last| +``` + +But permutation identities use: + +```text +z(omega^{rho_last} x) +``` + +And the memory planner uses `|M.rotationLast|` as a count. + +Impact: `|rho_last|` could mean absolute value of a rotation, cardinality of a +rotation set, or list length. These are not interchangeable. + +Fix: use distinct names: + +```text +rho_last_value // actual rotation exponent +num_rotation_last // count of last-rotation entries +omega_inv_to_l = omega^{-rho_last_value} +``` + +#### 15. ABI canonicality is not fully specified + +Section 7 explicitly requires the first head word to be `0x40`, exact proof +length, exact instance length, and no trailing calldata. But canonical ABI also +requires the second dynamic head to point exactly after the padded proof blob, +and the `bytes` padding should be checked if canonical encoding is required. + +Impact: calldata malleability and implementation divergence. One verifier may +accept nonzero proof padding or weird dynamic heads; another may reject. + +Fix: state exact checks: + +```text +head0 == 0x40 +head1 == 0x60 + ceilword(proof_len) +proof_padding_bytes == 0 +instances_len == EXPECTED_NUM_INSTANCES +calldatasize == 0x04 + head1 + 0x20 + 32 * instances_len +``` + +Also require `head1` not to overlap the proof area. + +#### 16. Simple selector lowering is unsafe unless selector usage is restricted + +The spec says simple-selector fixed-column queries lower to constant `1` and +are excluded from proof-evaluation reads. + +That is correct only if those fixed-column queries occur solely as the gate's +selector factor and are later reintroduced through selector buckets. + +Impact: if a simple-selector fixed column is referenced as a normal fixed +expression anywhere else, lowering it to `1` changes the quotient identity. + +Fix: add validation: + +```text +A simple-selector fixed column may only occur as the designated selector of its +own gate. +Any other query to that fixed column is inadmissible. +``` + +Or treat such columns as non-simple fixed columns when queried normally. + +#### 17. Native quotient callbacks are not fully validated by the bytecode validator + +The VM validator checks stack shape, but it cannot verify that: + +- `native_permutation` folds exactly the permutation identity range; +- `native_lookup` folds exactly the lookup identity range; +- `native_identity(i)` corresponds to the declared global identity position; +- callbacks advance selector/main accumulators exactly as the interpreted stream + would. + +Impact: native callbacks are correctness-critical but mostly outside the +bytecode validation model. + +Fix: add a native-callback manifest: + +```text +callback_id +start_identity_index +identity_count +target sequence +required scratch words +``` + +Then require validation that callback markers exactly cover their declared +identity ranges without overlap or gaps. + +#### 18. External quotient frame base is underdefined + +Section 11 defines: + +```text +F0 = VK_MPTR +F1 = max(VK_MPTR + |VK_payload|, REVERSED_EVALS_MPTR + W*E) +``` + +Then later uses: + +```text +B_q = QUOTIENT_FRAME_BASE +L_q = QUOTIENT_FRAME_LEN +``` + +But it never explicitly states: + +```text +QUOTIENT_FRAME_BASE = F0 +QUOTIENT_FRAME_LEN = F1 - F0 +``` + +Impact: external evaluator implementations may copy the frame to a different +base than the absolute addresses expect. + +Fix: define those equalities normatively and require that all absolute memory +addresses used by the external evaluator are the same as in the main verifier +after copying. + +#### 19. Bad-challenge zero denominators are undefined in KZG batching + +Section 8 computes: + +```text +d_j = product_{p in P_j}(x3 - p) +t_j = (a_j - R_j(x3)) * d_j^{-1} +``` + +But it does not say what happens if `x3` equals one of the rotation points. + +Impact: inversion of zero is undefined. An implementation using +`modexp(0, r-2, r)` will get `0`, silently changing the equation. + +Fix: require: + +```text +if d_j == 0: revert +``` + +This is a rare random-oracle edge case, but the verifier spec should still be +total. + +### Smaller but real spec bugs + +#### 20. Accumulator text contradicts itself + +Section 4 says a point-and-scalar accumulator tail contains two points plus two +scalar words. Later, Section 12 says: + +```text +The current accumulator encoding contains two point coordinates and two carried +scalars. +``` + +"Two point coordinates" is one affine point, not two points. + +Fix: change it to: + +```text +two affine points, i.e. four base-field coordinates, and two carried scalar +words +``` + +#### 21. Seven-limb arithmetic text contradicts the opcodes + +Section 12 says limb-specialized quotient VM instructions use: + +```text +seven limbs, six linear coefficients, 49 schoolbook products, and 13 output +diagonals +``` + +But `LIN7`, `BILIN7_ROW`, `BILIN7_PAIRWISE`, and `MODARITH7` use seven +coefficient/memory pairs or 13 diagonal coefficients. + +Impact: implementers cannot know whether six or seven linear coefficients are +expected. + +Fix: correct the count or explain which coefficient is implicit. + +#### 22. `num_instances` is ambiguous with two instance columns + +Admissible inputs require exactly two instance columns: one committed and one +non-committed. The ABI has a single `uint256[] instances`, and the VK header has +`num_instances`. + +Impact: `num_instances` could mean total instance cells across both columns, or +only non-committed public-input scalars. + +Fix: define: + +```text +num_instances = length of the non-committed public-input vector supplied in ABI +``` + +And separately define whether committed-instance-column evaluations are always +proof scalars, always zero, or derived from some other source. + +#### 23. The synthetic linearization query is underspecified + +The spec says the synthetic linearization query must be last and is expanded +into quotient-limb terms and simple-selector terms. But it does not fully +define: + +- its rotation point; +- its claimed scalar evaluation; +- exact quotient-limb coefficients; +- exact selector commitment coefficients; +- whether identity commitments are allowed in it; +- how it is represented in the query-to-evaluation map. + +Impact: this is PCS-critical and must be deterministic. + +Fix: add a subsection defining the synthetic query as an explicit virtual +commitment/evaluation pair and its expansion into MSM terms. + +### Summary + +The biggest correctness blockers are: + +1. quotient state/stack aliasing; +2. scalar-inversion scratch violating reserved memory; +3. missing quotient program byte length; +4. missing accumulator validation semantics; +5. undefined `x_split`; +6. ambiguous dummy evaluation counts; +7. missing query-to-evaluation mapping. + +These should be fixed before treating the document as a conformance spec. The +rest are mostly edge-case, interoperability, or validation issues, but several +could still become soundness bugs in a generated verifier. + +[1]: https://eips.ethereum.org/EIPS/eip-2537 "EIP-2537: Precompile for BLS12-381 curve operations" diff --git a/proofs/solidity-verifier/docs/audit/CODEGEN_ASSURANCE_DOSSIER.md b/proofs/solidity-verifier/docs/audit/CODEGEN_ASSURANCE_DOSSIER.md new file mode 100644 index 000000000..5669216d2 --- /dev/null +++ b/proofs/solidity-verifier/docs/audit/CODEGEN_ASSURANCE_DOSSIER.md @@ -0,0 +1,164 @@ +# Codegen Correctness And Security Assurance Dossier + +Assessed on 2026-05-11 for this repository's Midfall/Midnight Halo2 Solidity +verifier generator. + +## 1. Claim And Protocol Envelope + +The defensible claim is intentionally narrow: + +```text +For the supported Midfall profile and pinned generated artifacts, the generated +Solidity verifier is a faithful lowering of the Rust verifier in +../midfall/proofs/src, and no known exploitable security issue remains after +the audit, trace, mutation, and reproducibility gates in this dossier pass. +``` + +This is not a claim that the generator is a generic Halo2 verifier. The +supported profile is: + +- BLS12-381 KZG over `midnight_curves::Bls12` and scalar field `Fq`. +- Midfall Keccak transcript with committed instances enabled. +- One proof, one identity-committed instance column, and one direct public + instance column. +- No rotated non-committed instance queries. +- Matching Rust/Solidity feature flags for truncated challenges, fewer point + sets, and outer single-H quotient commitments. +- EIP-2537 BLS12-381 precompiles available with Prague-compatible semantics. +- VK bytecode and optional quotient evaluator bytecode pinned by runtime length + and codehash. + +The proof about any production deployment must be made over a fixed artifact +manifest: + +| Input | Required record | +| --- | --- | +| Midfall revision | `53dc872f495104046d96bdac0a690f903dc0c537` | +| Rust toolchain | `rust-toolchain.toml` | +| Cargo features | Exact feature list used to generate the verifier | +| Solidity compiler | `solc 0.8.30+commit.73712a01` | +| Solc flags | `--bin --optimize --via-ir --evm-version cancun --no-cbor-metadata` | +| Generated sources | Hashes of verifier, VK, and optional quotient evaluator sources | +| Runtime bytecode | Runtime length and `keccak256` for each deployed artifact | +| VK binding | VK digest plus external VK runtime length/codehash when split | +| Quotient binding | Evaluator runtime length/codehash when split | +| Proof ABI | `verifyProof(bytes proof, uint256[] instances)` | +| SRS assumption | Midnight SRS asset names, sizes, and expected source | + +Application-level statement binding is out of scope for raw `verifyProof`. +Wrappers must separately bind chain/domain, caller, state roots, action hashes, +nullifiers, replay windows, and any application-specific liveness rules. + +## 2. Rust To Solidity Semantic Map + +The generated verifier is justified by semantic checkpoints rather than by a +line-for-line translation. + +| Checkpoint | Rust source of truth | Solidity/codegen owner | Evidence expected | +| --- | --- | --- | --- | +| Parser and proof reads | `plonk/verifier.rs::{parse_trace,verify_algebraic_constraints}` | `src/codegen/protocol.rs`, `src/codegen/proof_layout.rs`, `templates/contracts/Halo2Verifier.sol` | Exact proof length, ABI head checks, per-section offsets, canonical scalar and G1 checks | +| Transcript challenges | `transcript/mod.rs`, `transcript/implementors.rs` | `src/transcript.rs`, `templates/contracts/Halo2Verifier.sol` | Rust/Solidity trace equality for VK, instances, commitments, and all challenges | +| Quotient identities | `plonk/mod.rs::partially_evaluate_identities` | `src/codegen/evaluator.rs`, `src/codegen/quotient/mod.rs`, `templates/partials/quotient_numerator/QuotientNumeratorBlock.yul` | Identity order manifest, quotient trace ids, VM validator, selector-fold trace coverage | +| Linearization | `plonk/linearization/verifier.rs::compute_linearization_commitment` | `src/codegen/generator.rs`, `src/codegen/pcs.rs` | Same `-nu_y(x)` scalar, selector buckets, quotient limb scalars, and commitment expansion | +| KZG multi-open | `poly/kzg/mod.rs::multi_prepare` | `src/codegen/pcs.rs` | Same dummy queries, point-set sort, `x1..x4`, `f_com`, `q_evals`, `pi`, final MSM | +| Final pairing | `poly/kzg/msm.rs::DualMSM::check` | `templates/contracts/Halo2Verifier.sol`, `src/codegen/pcs.rs` | Pairing precompile success, return size, and semantic result word checked | +| Optional accumulator | Wrapper logic outside `plonk/verifier.rs` | `AccumulatorEncoding`, VK header, `templates/contracts/Halo2Verifier.sol` | Public-input schema validation and batched accumulator/KZG pairing equation | + +The bridge between Solidity calldata and Rust verifier objects is: + +- Solidity scalar words are canonical `Fr` elements and are rejected when + `>= Fr::MODULUS`. +- Solidity G1 commitments are EIP-2537 padded uncompressed points; the off-chain + repacker derives them from native compressed Midfall proof bytes. +- Solidity public inputs are big-endian field words that correspond to the + Rust `instances` slice after the committed/non-committed split. +- The Solidity verifier absorbs the same mathematical transcript objects as + Rust, even where the physical proof encoding differs. + +## 3. Security Argument And Threat Model + +The no-false-accepts property is the primary security goal: + +```text +If the generated Solidity verifier returns true, then the pinned Midfall Rust +verifier would accept the same mathematical proof for the same VK and public +inputs, under the same feature profile. +``` + +Required controls: + +- Unsupported shapes fail in `SolidityGenerator::try_new` before rendering. +- VK and quotient dependencies are pinned by bytecode length and codehash. +- The proof parser rejects malformed ABI heads, wrong proof length, trailing + bytes, non-canonical scalars, and non-canonical/off-curve G1 coordinates. +- The transcript absorbs VK digest, committed identity instance, public input + length, public input scalars, and every prover message before the next + challenge. +- The quotient VM safety pass rejects unknown opcodes, truncated operands, + invalid memory tokens, stack underflow/leaks, and logical operand-width + corruption. +- The memory planner models permanent and phase-scoped ranges, including the + external quotient return buffer, and rejects live overlap. +- Production render paths remain `external view` and do not emit trace or gas + logs. Trace and gas-checkpoint paths are debug surfaces only. +- EIP-2537 wrappers check call success, exact return size, and semantic pairing + output. +- Accumulator inputs are validated before batching; malformed identity, + zero-scalar, and one-scalar paths still route points through EIP-2537 + validation. + +Explicit exclusions: + +- Generic committed-instance commitments other than the identity-commitment + zk_stdlib profile. +- Generic multi-proof verification and arbitrary instance-column layouts. +- Application wrapper soundness, replay protection, and state-machine liveness. +- Chains or L2s without the expected EIP-2537 semantics. + +## 4. Audit And Test Evidence + +`AUDIT_FINDINGS.md` is the open-issues ledger. A release cannot claim this +dossier until every finding is either fixed with code/test evidence or explicitly +excluded from production scope. + +Required gates: + +```bash +cargo test --workspace --all-features --all-targets +``` + +```bash +HALO2_SOLIDITY_RUN_EVM_TESTS=1 \ +cargo test --release --features evm,truncated-challenges,rust-verifier-trace -- --nocapture +``` + +```bash +HALO2_SOLIDITY_RUN_IVC_BENCH=1 \ +cargo test --release --features evm,truncated-challenges,fewer-point-sets,rust-verifier-trace \ + --test ivc_keccak_solidity -- --nocapture +``` + +Negative evidence must include: + +- Every proof scalar offset: `Fr`, `Fr + 1`, high-bit, and random + non-canonical words reject. +- Every proof G1 offset: nonzero top padding, coordinate `p`, off-curve point, + and malformed infinity policy reject. +- Every public input slot mutates to rejection unless it is intentionally unused + by a documented wrapper. +- Every VK and quotient section mutation rejects by constructor pinning or + proof failure. +- ABI variants with wrong selector, shifted dynamic heads, wrong array length, + trailing bytes, and truncated proof reject. +- Precompile failure, short return data, false pairing return, and stale memory + paths reject. +- Accumulator limb, coordinate, scalar, and point-pair mutations reject for + accumulator-enabled profiles. + +Release evidence should attach: + +- Test command logs and feature lists. +- Rust/Solidity trace comparison summary. +- Generated artifact hashes and runtime sizes. +- Pinned solc identity and compile flags. +- Any intentionally excluded debug-only or wrapper-only risk. diff --git a/proofs/solidity-verifier/docs/audit/REVIEW_PACKET.md b/proofs/solidity-verifier/docs/audit/REVIEW_PACKET.md new file mode 100644 index 000000000..fb9739587 --- /dev/null +++ b/proofs/solidity-verifier/docs/audit/REVIEW_PACKET.md @@ -0,0 +1,252 @@ +# Solidity Verifier Review Packet + +This packet is meant to make review tractable for cryptography researchers and +Solidity/security engineers. It turns the task from "read the whole codebase" +into "validate these bounded claims with these artifacts and checkpoints." + +## 1. Review Goal + +Primary claim to review: + +```text +For the supported Midfall profile and pinned generated artifacts, if the +generated Solidity verifier accepts, then the pinned Rust verifier would accept +the same mathematical proof for the same verifying key and public inputs. +``` + +This review is not a generic Halo2 verifier audit. The generator is specialized +to the Midfall/Midnight proof profile documented in +`CODEGEN_ASSURANCE_DOSSIER.md`. + +## 2. Scope + +In scope: + +- Rust lowering and artifact generation under `proofs/solidity-verifier/src`. +- Generated Solidity/Yul templates under `proofs/solidity-verifier/templates`. +- Solidity calldata repacking and proof/public-input encoding. +- VK, quotient evaluator, and verifier bytecode binding. +- BLS12-381 EIP-2537 precompile usage and failure handling. +- Existing fixtures, trace comparison machinery, and EVM tests. + +Out of scope unless explicitly added: + +- Generic Halo2 circuit support. +- Application wrapper security, replay protection, nullifier policy, or state + transition semantics. +- Chains or L2s without Prague-compatible EIP-2537 semantics. +- Arbitrary committed-instance commitments beyond the supported identity + committed instance profile. + +## 3. Supported Protocol Envelope + +The intended production profile is: + +- BLS12-381 KZG over `midnight_curves::Bls12`. +- Scalar field `midnight_curves::Fq`. +- Midfall Keccak transcript. +- One proof. +- Exactly one identity-committed instance column. +- Exactly one direct public instance column. +- No rotated non-committed instance queries. +- Matching Rust/Solidity features for truncated challenges, fewer point sets, + and outer single-H quotient commitments. +- Optional split VK and quotient evaluator contracts pinned by runtime length + and codehash. + +Any artifact outside this envelope should be treated as unsupported unless the +scope is updated. + +## 4. Artifact Manifest + +Fill this table for the exact artifact under review. Use +`docs/reference/REPRODUCIBLE_BUILDS.md` for the recorded IVC benchmark hashes. + +| Item | Value | +| --- | --- | +| Repository commit | `a096e71746e401404f250817ca4e857bac1eef56` | +| Working tree status at packet creation | clean before this packet was added | +| Rust toolchain | `rust-toolchain.toml` | +| Solidity compiler | `solc 0.8.30+commit.73712a01` | +| Solidity flags | `--bin --optimize --via-ir --evm-version cancun --no-cbor-metadata` | +| Cargo features | fill per artifact | +| Generated verifier source hash | fill per artifact | +| Generated VK source hash | fill per artifact, if split | +| Generated quotient evaluator source hash | fill per artifact, if split | +| Verifier runtime length/hash | fill per artifact | +| VK runtime length/hash | fill per artifact, if split | +| Quotient evaluator runtime length/hash | fill per artifact, if split | +| Proof fixture hash | fill per artifact | +| Public input fixture hash | fill per artifact | +| Calldata hash | fill per artifact | + +The currently recorded IVC benchmark manifests live in +`docs/reference/REPRODUCIBLE_BUILDS.md`. + +## 5. Reading Order + +Start here: + +1. `docs/audit/CODEGEN_ASSURANCE_DOSSIER.md` +2. `docs/reference/STATUS.md` +3. `docs/architecture/LOWERING_ARCHITECTURE_SPEC.md` +4. The generated Solidity artifact(s) under review. +5. The source files for the specific checkpoint being reviewed. + +Avoid starting with the large Yul templates. First understand the semantic +claim, supported profile, and checkpoint map. + +## 6. Crypto Researcher Checklist + +The crypto review should answer these questions: + +- Does the Solidity transcript absorb the same mathematical objects as the Rust + verifier, in the same order, before each dependent challenge? +- Are the VK digest, instance length, committed identity instance, public + inputs, commitments, evaluations, `f_com`, `q_evals`, and `pi` absorbed at + the correct points? +- Is the quotient numerator algebra equivalent to the Rust Halo2 identities for + gates, permutation, lookup/logup, and trash constraints? +- Are selector folds and lookup/permutation chunk boundaries batched with the + intended powers? +- Is the linearization commitment relation equivalent to + `plonk/linearization/verifier.rs`? +- Is the KZG `multi_prepare` batching equivalent, including dummy queries, + point-set order, `x1`, `x2`, `x3`, `x4`, `f_com`, `q_evals`, and `pi`? +- Do truncated-challenge and fewer-point-set feature choices match the native + proof being checked? +- Is the final pairing equation algebraically equivalent to the Rust + `DualMSM::check` equation? +- If accumulator support is enabled, is the carried accumulator batching sound + and bound to the same public inputs? + +The target answer is not "the code looks reasonable." The target answer is: + +```text +For this artifact and feature profile, Solidity acceptance implies Rust verifier +acceptance for the same mathematical statement. +``` + +## 7. Solidity And Systems Checklist + +The implementation review should answer these questions: + +- Does the ABI parser reject wrong selectors, malformed dynamic heads, truncated + proof bytes, trailing bytes, and wrong instance lengths? +- Are all `Fr` words checked canonical before use? +- Are all BLS12-381 G1 inputs range-checked and validated through EIP-2537 + before they can influence acceptance? +- Are padded EIP-2537 G1 encodings handled consistently as 128-byte values? +- Can any Yul memory region overlap live verifier state? +- Do scratch regions have generated capacity checks for the circuit dimensions? +- Do precompile wrappers check call success, exact return size, and semantic + result words? +- Can trace or gas-checkpoint logic perturb production acceptance? +- Are VK and quotient evaluator bytecode length/codehash pinned before use? +- Does any unsupported circuit shape fail before rendering? + +## 8. Semantic Checkpoint Map + +Use this table as the shared bridge between researchers and implementers. + +| Checkpoint | Rust source of truth | Solidity/codegen owner | Evidence to request | +| --- | --- | --- | --- | +| Proof parser and transcript schedule | `proofs/src/plonk/verifier.rs` | `src/lowering/protocol`, `src/lowering/abi`, `TranscriptProofParser.yul` | Proof offset table and transcript trace | +| VK digest | `proofs/src/plonk/mod.rs` | `src/lowering/vk.rs`, `VkLoading.yul` | VK digest equality and runtime pinning | +| Quotient identities | `proofs/src/plonk/mod.rs` | `src/lowering/quotient.rs`, `src/lowering/quotient_numerator`, `QuotientNumeratorBlock.yul` | Per-identity numerator trace | +| Linearization | `proofs/src/plonk/linearization/verifier.rs` | `QuotientAndLinearization.yul`, `src/lowering/kzg` | Linearization scalar and commitment terms | +| KZG multi-open | `proofs/src/poly/kzg/mod.rs` | `src/lowering/kzg/mod.rs`, `Pcs.yul` | Query schedule and final MSM terms | +| Pairing check | `proofs/src/poly/kzg/msm.rs` | `FinalPairing.yul` | Pairing inputs and precompile result | +| Accumulator, if enabled | wrapper logic outside Rust PLONK verifier | `AccumulatorHelpers.yul`, `src/lowering/encoding` | Public-input decoding and batched pairing terms | + +## 9. Trace Evidence Template + +For each reviewed artifact, provide a trace table with Rust and Solidity values. + +| Step | Rust value | Solidity value | Source/checkpoint | +| --- | --- | --- | --- | +| VK digest | fill | fill | transcript start | +| public input length | fill | fill | transcript instances | +| beta | fill | fill | after first commitment phase | +| gamma | fill | fill | after beta | +| y | fill | fill | quotient challenge | +| x | fill | fill | evaluation challenge | +| quotient numerator | fill | fill | quotient evaluator | +| linearization scalar(s) | fill | fill | linearization | +| `f_com` | fill | fill | KZG multi-open | +| `q_evals` | fill | fill | KZG multi-open | +| final MSM lhs | fill | fill | pairing input | +| final MSM rhs | fill | fill | pairing input | +| pairing result | fill | fill | final return | + +This table is the fastest way for cryptographers to review the implementation +without mentally executing every Yul block. + +## 10. Negative Test Matrix + +The review packet should include logs or fixtures showing rejection for: + +- Every proof scalar mutated to `Fr`, `Fr + 1`, high-bit, and random + non-canonical values. +- Every proof G1 mutated with nonzero top padding, coordinate `p`, off-curve + coordinates, and malformed infinity encodings. +- Public input mutations at every slot. +- Wrong proof length, truncated proof, trailing proof bytes, wrong ABI selector, + shifted ABI heads, and wrong array length. +- VK runtime mutation or wrong VK codehash. +- Quotient evaluator runtime mutation or wrong evaluator codehash. +- Precompile failure, short returndata, and false pairing result. +- Accumulator limb, scalar, coordinate, and point-pair mutations when the + accumulator path is enabled. + +## 11. Reproduction Commands + +List tests without running: + +```bash +cargo test -p halo2_solidity_verifier --all-features --all-targets -- --list +``` + +Run the normal verifier suite: + +```bash +cargo test -p halo2_solidity_verifier --all-features --all-targets -- --nocapture +``` + +Run Solidity/EVM tests: + +```bash +HALO2_SOLIDITY_RUN_EVM_TESTS=1 \ +SRS_DIR=/path/to/midfall/zk_stdlib/examples/assets \ +cargo test -p halo2_solidity_verifier --release \ + --features evm,rust-verifier-trace --lib test:: -- --nocapture +``` + +Run the Poseidon integration fixture: + +```bash +HALO2_SOLIDITY_RUN_EVM_TESTS=1 \ +SRS_DIR=/path/to/midfall/zk_stdlib/examples/assets \ +cargo test -p halo2_solidity_verifier --release \ + --features evm,truncated-challenges --test poseidon_fixture -- --nocapture +``` + +Run the IVC Solidity benchmark and trace path: + +```bash +SRS_DIR=/path/to/midfall/zk_stdlib/examples/assets \ +proofs/solidity-verifier/scripts/run_ivc_bench.sh --trace --no-gas-checkpoints +``` + +## 12. Expected Review Outputs + +Ask reviewers to produce: + +- A short statement of the exact artifact and feature profile reviewed. +- A list of accepted assumptions and exclusions. +- Answers to the crypto checklist and systems checklist. +- Any finding with severity, affected checkpoint, reproduction steps, and + whether it can cause false acceptance. +- A final statement of whether the primary claim is supported, unsupported, or + supported only after fixes. + diff --git a/proofs/solidity-verifier/docs/benchmarks/BENCH.md b/proofs/solidity-verifier/docs/benchmarks/BENCH.md new file mode 100644 index 000000000..d8fc67613 --- /dev/null +++ b/proofs/solidity-verifier/docs/benchmarks/BENCH.md @@ -0,0 +1,626 @@ +# Per-section gas attribution + +Measured breakdowns of the Poseidon-fixture verifier and the IVC Keccak final +verifier on the midfall branch, captured via the `solidity-gas-checkpoints` +cargo feature. +This document is the measurement counterpart to `OPTIMISATION.md`: where +that one lists *what changes are available*, this one says *which sections +are actually expensive enough to be worth changing*. + +The Midnight dependencies resolve from the published midfall branch: + +``` +https://github.com/EYBlockchain/midfall.git#keccak +``` + +The benchmark needs local SRS files. The helper script below downloads missing +assets into `.srs/` by default; set `SRS_DIR` to reuse an existing directory. +The current IVC Solidity bench needs Midnight `midnight-srs-2p19` for the leaf +IVC proofs and `midnight-srs-2p20` for the final tree-decider proof. + +## Running the Poseidon fixture bench + +The verifier emits a LOG1 at every section boundary when compiled with +`--features solidity-gas-checkpoints`. The host-side test +(`tests/poseidon_fixture.rs::dump_gas_checkpoints`) parses those logs +into a per-section delta table. + +``` +cargo test --features evm,solidity-gas-checkpoints \ + --test poseidon_fixture poseidon_renders_compiles_and_verifies \ + -- --ignored --nocapture +``` + +Topic encoding: `(id << 248) | gas()` — `id` lives in the upper byte and +the remaining 31 bytes hold the value of `gas()` at the moment the +checkpoint runs. Each LOG1 costs ~750 gas (16 sites × 750 = 12 kg total +overhead, subtracted from the printed deltas). + +The 16 checkpoints sit at semantic section boundaries — see +`templates/contracts/Halo2Verifier.sol` (search for `gas_checkpoint(`). + +## Running the IVC Keccak final bench + +The IVC bench proves two independent one-step IVC Poseidon hash-chain leaves, +then proves a final Keccak-transcript tree decider that verifies both leaf +proofs and fully collapses the carried IVC proof accumulator. It renders +separate verifier/VK contracts for that decider proof, compiles them with +solc, deploys them in Prague-spec revm, and verifies the final proof end to +end. It self-skips unless `HALO2_SOLIDITY_RUN_IVC_BENCH=1` is set because it +is slow. + +Recommended runner: + +``` +scripts/run_ivc_bench.sh +``` + +The default runner enables fewer point sets for the recursive in-circuit +verifier only. The outer Solidity-facing decider proof omits +`outer-fewer-point-sets`. + +Compile-only preflight: + +``` +scripts/run_ivc_bench.sh --check-only +``` + +Use an existing SRS directory or also run the off-circuit Midfall twin: + +``` +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ + scripts/run_ivc_bench.sh --native-midfall +``` + +Manual compile command: + +``` +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ +cargo test --release \ + --features evm,truncated-challenges,in-circuit-fewer-point-sets,solidity-gas-checkpoints \ + --test ivc_keccak_solidity ivc_final_keccak_solidity_e2e \ + --no-run +``` + +Manual full gas-checkpoint command: + +``` +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ +cargo test --release \ + --features evm,truncated-challenges,in-circuit-fewer-point-sets,solidity-gas-checkpoints \ + --test ivc_keccak_solidity ivc_final_keccak_solidity_e2e \ + -- --nocapture +``` + +The test writes generated artifacts to: + +``` +target/ivc-keccak-solidity-dump/ +``` + +Useful follow-up commands: + +``` +cat target/ivc-keccak-solidity-dump/contract-sizes.txt +ls -lh target/ivc-keccak-solidity-dump +``` + +### IVC Keccak final run, 2026-04-29 + +This historical measurement is the previous one-step SHA aggregation baseline. +Re-run the bench after fetching `midnight-srs-2p20` to populate current +Poseidon-chain tree decider numbers. + +Run shape: + +- 1 inner SHA proof: 0.59 s. +- IVC setup: 47.82 s. +- IVC final Keccak step: 64.22 s. +- Native `verify_final`: 10.01 ms. +- Compressed final proof: 9,952 bytes. +- Repacked EIP-2537-padded proof: 12,672 bytes. +- Calldata: 14,980 bytes, with 68 public-input field elements. + +Contract size summary with `SOLC_OPTIMIZE_RUNS = 1` and no CBOR metadata: + +> Historical inline-verifier baseline. Re-run `scripts/run_ivc_bench.sh` to +> refresh the contract sizes for the current circuit/VK. + +``` +Halo2Verifier.sol source bytes: 426,899 +Halo2VerifyingKey.sol source bytes: 27,237 +Halo2Verifier creation bytecode bytes: 53,571 +Halo2VerifyingKey creation bytecode bytes: 5,925 +Halo2Verifier deployed runtime bytes: 53,325 +Halo2VerifyingKey deployed runtime bytes: 6,752 +total deployed runtime bytes: 60,077 +``` + +Gas summary: + +``` +total tx gas_used = 1,617,543 +real section work = 1,374,422 +checkpoint overhead = 15,750 (21 checkpoints x 750 gas) +``` + +Largest sections: + +| section | gas | note | +|---|---:|---| +| PCS block 3 set 0 q_com/q_eval fold | 494,377 | biggest PCS fold | +| evaluations + transcript tail | 248,795 | eval reads, challenge squeezes, proof accumulator prep | +| linearization-commitment MSM | 119,469 | verifier linearization commitment | +| quotient evaluation | 107,966 | Fr arithmetic | +| public accumulator pairing check | 105,329 | trivial one-step outer accumulator; skips identity/zero-scalar MSM calls | +| final proof ec_pairing | 103,168 | final proof/KZG accumulator pairing after PCS inputs are already prepared | +| Lagrange + instance evaluation | 55,002 | public instance evaluation | + +The application-level accumulator carried in the decider state is now fully +collapsed over the inner verifier's fixed bases: it exposes only +`lhs point, lhs scalar = 1, rhs point, rhs scalar = 1`. The outer IVC +self-accumulator still follows Midfall's variable-base-collapsed shape, because +its fixed bases are the self VK; for the one-step final proof it is trivial, so +the Solidity verifier detects the packed identity encoding and skips +identity/zero-scalar MSM precompile calls. + +## Measured breakdown (Poseidon fixture, k=6, midfall HEAD) + +The breakdown below was captured **after** Step 6 + Optimisations B/D/E +(the `byte_reverse_32` 31-iter unroll plus PCS-block tuning, see +OPTIMISATION.md). The Step-5 baseline (1,475,560 gas before any of +this) is preserved as a comparison column — every section that +touched calldata-backed evals saw 5-30× reductions when Step 6 dropped +the per-call cost of `byte_reverse_32` from ~700 gas to ~140 gas, and +the PCS block saw a further 17 kg cut from B/D/E. + +### Step 6 + B/D/E + H1/H2 (current HEAD) + +``` +=== gas-checkpoint breakdown (per-section deltas) === + id gas_left delta % section + 1 49,916,089 - - entry (before VK loading) + 2 49,913,842 1,497 0.2% VK loading + 3 49,910,536 2,556 0.3% VK digest + committed_pi + instance absorbs + 4 49,906,596 3,190 0.4% user-phase advice reads + user challenge squeezes + 5 49,903,282 2,564 0.3% theta squeeze + lookup multiplicities + 6 49,898,123 4,409 0.5% beta/gamma + permutation Z products + 7 49,896,797 576 0.1% lookup helpers + Z accumulators + 8 49,893,704 2,343 0.3% trash_challenge + trashcans + 9 49,890,004 2,950 0.4% y squeeze + quotient-limb reads + 10 49,834,193 55,061 6.7% evaluations + x1/x2 + f_com + x3 + q_evals + x4 + pi + 11 49,824,712 8,731 1.1% Lagrange + instance evaluation + 12 49,700,363 123,599 15.0% quotient evaluation (Fr arithmetic) + 13 49,627,335 72,278 8.8% linearization-commitment MSM + 17 49,626,337 248 0.0% PCS block 1 (rotation points x*omega^rot) + 18 49,621,859 3,728 0.5% PCS block 2 (x1 powers, ROLLED LOOP) + 19 49,343,701 277,408 33.7% PCS block 3 set 0 q_com fold (m=33 MSM, MCOPY) + 20 49,288,465 54,486 6.6% PCS block 3 set 1 q_com fold (m=5 MSM, MCOPY) + 21 49,259,880 27,835 3.4% PCS block 3 set 2 q_com fold (m=2 MSM, MCOPY) + 22 49,248,489 10,641 1.3% PCS block 4 (f_eval Lagrange interpolation) + 23 49,206,985 40,754 5.0% PCS block 5 (final_com x4-power MSM + v) + 14 49,180,800 25,435 3.1% PCS block 6 (pairing inputs LHS/RHS) + 15 49,180,011 39 0.0% accumulator random-combine + 16 49,076,093 103,168 12.5% final ec_pairing + + cp1 gas_left = 49,916,089 (verifier entry) + cp16..cp1 gas billed = 839,996 (work between cp1 and cp16) + - measurement overhead = 17,250 (23 checkpoints x 750 gas) + = real section work = 822,746 + total tx gas_used = 924,703 (incl. tx base + calldata + pre-cp1 + post-cp16) +``` + +PCS sub-block totals: cp17..cp23 + cp14 = 440,535 gas (vs 513,714 +pre-H1/H2). The cp14 PCS bucket dropped from 514 kg to 441 kg +(−73 kg, −14 %). All non-PCS sections within ±20 gas of pre-H1/H2. + +Per-section deltas vs Step 6 baseline: +- cp14 PCS block −88,986 (−15,173 from B + −189 from E + −572 from + D + −25,021 from H1 + −47,981 from H2 + downstream solc-via-ir + re-scheduling effects). +- cp16 final pairing −1,262 (E in `ec_pairing`). +- All other sections within ±20 gas of Step 6 baseline (build-noise + / measurement jitter). + +### Fine-grained PCS sub-block attribution (cp17..cp23, cp14) + +The PCS section has additional checkpoints inside it that attribute +the 441-kg cp14 bucket to each of the 8 emitter sub-blocks (one +output of `pcs_computations()`). For the Poseidon fixture (3 point +sets) the emitter produces: + +``` +=== PCS sub-block deltas (cp17..cp23, cp14) — current HEAD (B/D/E/H1/H2) === + id delta %_PCS %_total section + 17 248 0.1% 0.03% block 1: rotation points (x*omega^rot, 3 distinct) + 18 3,728 0.8% 0.4% block 2: x1 powers (33 muls + mstore, ROLLED LOOP) + 19 277,408 62.9% 30.0% block 3 set 0 q_com fold (m=33 MSM, MCOPY staging) + 20 54,486 12.4% 5.9% block 3 set 1 q_com fold (m=5 MSM, MCOPY staging) + 21 27,835 6.3% 3.0% block 3 set 2 q_com fold (m=2 MSM, MCOPY staging) + 22 10,641 2.4% 1.2% block 4: f_eval Lagrange interpolation (3 sets) + 23 40,754 9.2% 4.4% block 5: final_com x4-power MSM + v + 14 25,435 5.8% 2.8% block 6: pairing inputs LHS = pi, RHS = final_com - v*G + x3*pi +total 440,535 100% 47.6% +``` + +Pre-H1/H2 cp14 was 514 kg; H1 (per-commit MCOPY staging) saved +~25 kg directly in cp19/cp20/cp21, and H2 (rolling block 2 x1-powers +into a Yul for-loop) saved another ~22 kg directly in cp18 plus +~25 kg downstream in cp19 from solc-via-ir re-scheduling the whole +PCS section more compactly when block 2 is no longer 32 unrolled +mulmod+mstore lines fighting block 3 for register slots. + +Per-EIP-2537 G1MSM precompile contributions (cost = `k * 12000 * +discount[k] / 1000` with the discount table from EIP-2537): + +| call | k | discount | precompile gas | block | EVM-side overhead | +|---|---:|---:|---:|---:|---:| +| set 0 q_com fold | 33 | 133 | 52,668 | cp19=329,365 | ~277 kg | +| set 1 q_com fold | 5 | 517 | 31,020 | cp20=54,630 | ~24 kg | +| set 2 q_com fold | 2 | 888 | 21,312 | cp21=27,907 | ~7 kg | +| block 5 final_com (3 × MSM-1 + 3 × G1ADD) | 1+1+1 | 1200 | 3 × 14,400 + 3 × 600 ≈ 45,000 | cp23=40,754 | ~−4 kg* | +| block 6 pairing RHS (2 × MSM-1 + 2 × G1ADD) | 1+1 | 1200 | 2 × 14,400 + 2 × 600 ≈ 30,000 | cp14=25,435 | ~−5 kg* | +| **PCS precompile subtotal** | | | **~ 180 kg** | of total **514 kg** | (35 %) | + +\* The negative "EVM overhead" for blocks 5 and 6 means the section +delta is *less* than the precompile-only cost — solc-via-ir is folding +the staging mstore chain into the precompile call directly, so the +block 5 and 6 deltas effectively measure precompile + a few mstores. +The EIP-2537 discount table is also slightly more aggressive than +the formula above for k=1 (some implementations cap at 12,000). + +Headline: **set 0's m=33 q_com fold is THE single biggest line in the +verifier**: 329 kg = 64 % of the PCS section = 36.8 % of total +verifier gas. The precompile itself only accounts for 53 kg (16 %) +of that 329 kg; the remaining 277 kg is EVM-side staging: + +- 32 × `byte_reverse_32(calldataload(...))` calls in the q_eval + Fr accumulator: ~4.5 kg (post-Step-6 unroll; was ~22 kg pre-unroll). +- 32 × `mulmod`/`addmod` in the q_eval Horner: ~0.5 kg. +- 33 × 5-mstore staging into MSM_SCRATCH (165 mstores from VK + region into 0x6100..0x75a0): solc-via-ir compiles each + `mstore(CONST, mload(VK_OFFSET))` to a ~30-50 gas EVM sequence + after constant folding + stack scheduling, so ~6-8 kg. +- Memory expansion (going from ~370 words to ~941 words): ~3 kg. +- Static-call overhead: ~1 kg. + +The remaining ~260 kg is "via-IR generated dispatch overhead" +similar in character to the pre-Step-6 `byte_reverse_32` cost +(many small Yul statements that solc inlines but with non-trivial +stack-juggling overhead). It's the next big optimization target — see +"Suggested optimisations" item I below. + +### Step 5 baseline (pre-unroll, for comparison) + +``` + delta (Step 5) delta (Step 6) reduction + 2 VK loading 1,497 1,497 0 + 3 VK digest + committed_pi + instance absorbs 8,193 2,556 -5,637 + 4 user-phase advice + challenge squeezes 3,166 3,190 +24 + 5 theta + lookup multiplicities 6,319 2,564 -3,755 + 6 beta/gamma + permutation Z 11,916 4,409 -7,507 + 7 lookup helpers + Z accumulators 570 576 +6 + 8 trash_challenge + trashcans 6,098 2,343 -3,755 + 9 y squeeze + quotient-limb reads 6,696 2,950 -3,746 + 10 evals + x1..x4 + q_evals + pi 169,674 55,061 -114,613 + 11 Lagrange + instance evaluation 8,766 8,731 -35 + 12 quotient evaluation 360,800 123,599 -237,201 <- biggest + 13 linearization MSM 72,293 72,293 0 + 14 PCS block 631,364 529,521 -101,843 + 15 accumulator combine 39 39 0 + 16 final pairing 104,430 104,430 0 + --------- --------- ---------- + total (cp16..cp1, with overhead) 1,403,071 925,009 -478,062 +``` + +The ~85 kg gap between `total tx gas_used` (1,010 kg) and the +inter-cp1/cp16 range (925 kg) is **tx-level fixed cost**: 21,000 base ++ ~67,000 calldata (4,484 bytes × 16 gas/non-zero) + ~1,100 +`extcodecopy` of the VK contract. + +## Where the gas actually goes (after Step 6) + +The verification cost now concentrates in three sections (PCS, +pairing, quotient eval), with the rest being small change. Split into +precompile work and EVM work: + +| section | gas | breakdown | +|---|---:|---| +| **PCS computation (cp14)** | **441 kg** | Now broken out by sub-block (cp17..cp23): **set 0 q_com fold = 277 kg (63 %)**, set 1 q_com fold = 54 kg, set 2 q_com fold = 28 kg, block 5 final_com = 41 kg, block 6 pairing inputs = 25 kg, f_eval Lagrange = 11 kg, x1 powers = 4 kg, rotation points = 0.2 kg. Per-EIP-2537 G1MSM precompile cost across all 8 PCS calls is **~180 kg (41 %)**, leaving **~261 kg (59 %) as EVM-side staging + Fr arithmetic + memory expansion**. Down 89 kg from Step 6 (B: −15 kg modexp, D: −0.6 kg mload, E: −0.2 kg mcopy, H1: −25 kg per-commit MCOPY, H2: −48 kg rolled x1-powers loop with downstream re-scheduling). | +| **Quotient evaluation (cp12)** | **124 kg** | ~587 `mulmod`/`addmod` sites in the gate evaluator. Step 6 dropped this from 361 kg by eliminating ~80 redundant 32-iter `byte_reverse_32` loops. | +| **Final pairing (cp16)** | 103 kg | EIP-2537 `BLS12_PAIRING_CHECK` for k=2: `32600 + 37700 × 2 = 108,000` minus measurement overhead. E (mcopy in `ec_pairing`) shaved ~1.3 kg of EVM overhead (point staging for the 0x300-byte input scratch); the precompile cost itself is the cryptographic floor and cannot be reduced. | +| **Linearization MSM (cp13)** | 72 kg | One 8-pair G1MSM (~33 kg) + Horner scalar prep (~30 mulmod chain) + 8-pair × 5-mstore staging. **Unchanged by Step 6 / B / D / E** (its calldata reads are not in the byte-reverse hot path; its mstore chains are inside an MSM emitter not yet retrofitted to MCOPY). | +| **Eval + transcript tail (cp10)** | 55 kg | 48 evals × ~10 gas (calldataload + byte_reverse + lt + common_word, post-unroll) + 4 keccak squeezes (challenge buffer ~1.6 KB → ~30 kg/squeeze) + 2 `common_uncompressed_g1` calls + memory growth. Step 6 cut ~115 kg here (was 170 kg). | +| transcript stage cp2..cp9 | ~16 kg | streaming-keccak absorb cycles, mostly. Step 6 cut these to a third of pre-unroll. | +| Lagrange + instance eval (cp11) | 9 kg | small batch invert + dot-product over instances | +| acc random-combine (cp15) | 0 kg | branch not taken (HAS_ACCUMULATOR_MPTR == 0 for poseidon) | +| **non-tx total** | **835 kg** | of which ~338 kg is precompile gas (40 %), ~497 kg is EVM (60 %). Cumulative EVM-side savings since the original 1,589 kg baseline: ~681 kg (largely from byte_reverse_32 unroll + Step 6 transcript work + B/H1/H2 in PCS). | + +## The "catch-all" was `byte_reverse_32` + +The pre-Step-6 breakdown attributed ~625 kg to "solc / inlining +overhead". Investigation showed it was **not** solc-inlining (the +optimizer-runs sweep was nearly insensitive between `runs=1` and +`runs=100000`, only ~2 kg difference). The actual cause was the +`byte_reverse_32` Yul helper, called ~184 times in the rendered +verifier and emitted as a 32-iteration `for { let i := 0 } lt(i,32) +{ i := add(i,1) } { ... }` shift loop: + +- per call: 32 iters × ~22 gas/iter + ~50 gas function-call overhead + ≈ 700–750 gas. +- 184 calls × 700 = ~129 kg directly visible. +- secondary effects (keccak input prep, modexp parameter framing, + etc.) added another ~350 kg of indirect cost. + +After Step 6 (31 of 32 iters unrolled into straight-line `byte() | shl` +ops, 1 trailing trip kept as a guard loop), each call drops to ~140 +gas (32 ops × ~3 gas + ~40 gas overhead). The trailing guard loop is +required because solc with `--via-ir` aggressively inlines fully +straight-line function bodies at every call site; for this verifier +that triggers a pathology where execution hits the 50 M block gas +limit. Keeping a single-iter loop preserves the function-call +boundary. + +Total measured saving: **478 kg (-32 % from baseline)**. + +`--optimize-runs` was bumped from `1` to `200` as part of this work +(see `src/evm.rs::DEFAULT_OPTIMIZE_RUNS`), but the two changes are +nearly orthogonal: bumping runs alone saves ~800 gas, the unroll alone +saves ~478 kg. The runs bump is kept for two reasons: (1) it is now +safe (post-Step-5 the contract size dropped well below the 24 kB +limit, so deployment-cost bias is no longer needed), and (2) it lets +ad-hoc A/B measurement via `SOLC_OPTIMIZE_RUNS=N`. + +## Suggested optimisations (ordered by ROI) + +### A. ~~`decompress_g1` library + bump `--optimize-runs`~~ (closed) + +**Status:** investigated, closed. `decompress_g1` was already removed +in Step 5 (the on-chain compressed→uncompressed path is gone). An +A/B sweep of `--optimize-runs ∈ {1, 50, 200, 1000, 100000}` showed +only ~2 kg sensitivity — `--via-ir` already optimises aggressively +regardless of the runs setting. The default has been raised from `1` +to `200` for hygiene (it's now safe — post-Step-5 the contract is +well under 24 kB) but the gas impact is negligible. + +The actual top item turned out to be `byte_reverse_32`: see Step 6 +in OPTIMISATION.md, **−478 kg measured**. That made A's projection +of 200–400 kg moot. + +### B. Montgomery batched scalar inversion +**Projection: 12–18 kg saved** + +The PCS block makes 14 separate `scalar_inv(x_i)` calls (each ~1.4 kg +of modexp gas + EVM overhead). Replace with one Montgomery batch +invert: + +```yul +// Pseudo-Yul: invert {x_0, x_1, …, x_{n-1}} in O(n) muls + 1 modexp +let prod := x_0 +let cum_0 := prod +prod := mulmod(prod, x_1, r); let cum_1 := prod +… +let prod_inv := scalar_inv(prod) +// Walk back: each x_i_inv := prod_inv * cum_{i-1} ; prod_inv := prod_inv * x_i +``` + +Saves 13 modexp calls (~13 × 1.4 kg = ~18 kg) at the cost of ~28 muls +(~280 gas). Net ~17 kg. + +**Files:** `src/codegen/pcs.rs` (the emitter that lays out the +14 `let _ := scalar_inv(_)` lines in the PCS block). Probably a +dedicated `batch_scalar_inv` helper in the Yul prelude. + +**Risk:** all inputs must be non-zero. For the PCS Lagrange basis and +`dx` denominators this is guaranteed by Fiat-Shamir (the challenges +are uniform and the basis is over distinct points), but worth keeping +the per-input zero check for defence in depth (revert if any is zero). + +### C. Pre-fold `mulmod(_, 1)` / `addmod(_, 0)` in the evaluator codegen +**Projection: 5–15 kg saved** + +The gate evaluator (`src/codegen/evaluator.rs`) emits `mulmod(x, 1, r)` +and `addmod(x, 0, r)` whenever a multiplicative or additive identity +appears in the constraint. With `runs=1` solc cannot constant-fold +these. Add a pass at codegen time that drops them. + +**Files:** `src/codegen/evaluator.rs` (the `evaluate` recursion that +emits per-expression Yul lines). + +**Risk:** must distinguish "literally constant `1`" (drop) from +"`mload(SOME_MPTR)` that *happens* to evaluate to `1` for this circuit" +(keep — it's a domain quantity, not a literal). Easy if the evaluator +already tracks ConstantExpression nodes separately. + +### D. Hoist `mload(Y_MPTR)` and rotation slots in the evaluator +**Projection: 5–15 kg saved (subsumed by A but still wins on `runs=1`)** + +The gate evaluator currently re-mloads `Y_MPTR`, `THETA_MPTR`, +`BETA_MPTR`, etc. inside the inner Horner step of every identity +(~50 identities × ~5 mloads each = ~250 redundant mloads at 3 gas = +0.75 kg). Cheap to fix at codegen time — emit a `let y := mload(Y_MPTR)` +at the top of the quotient block and reference `y` in each step. + +**Files:** `src/codegen.rs` (the `make_block` closure that emits each +identity's Horner step), and `src/codegen/evaluator.rs` (the part that +substitutes `Y_MPTR` → local `y`). + +**Risk:** none, mechanical. + +### E. MCOPY the EC point staging (#5 in OPTIMISATION.md) +**Projection: 5–10 kg saved** + +The 5 single-pair MSMs in the PCS Block 5 / Block 6 + the 5 G1ADDs +each repeat a 4-line `mstore(0x180, mload(...))` chain to copy +4-word points. Cancun ships MCOPY (`0x5e`); replace each chain with +one `mcopy(dst, src, 0x80)`. + +**Files:** `templates/contracts/Halo2Verifier.sol` and the `pcs_computations` +emitter in `src/codegen/pcs.rs`. + +**Risk:** none. EVM target is already Cancun. + +### F. `truncated-challenges` (128-bit Fr challenges) — opt-in only +**Projection: 100–200 kg saved** + +With 128-bit truncated challenges every `mulmod(scalar, x_i, r)` in the +gate evaluator and Horner folds becomes a 128×256 mulmod, and many of +the chains can collapse one or two operations earlier. snark-verifier +reports this saves ~150 kg on a comparable Poseidon proof (PR #9). + +**Caveat:** drops Fiat-Shamir security from 256-bit to 128-bit. The +prover and verifier must both opt in. Worth measuring behind a feature +flag (`truncated-challenges`) but **not** the default path. + +**Files:** prover side (`midfall/proofs/src/transcript`), verifier +codegen (every `squeeze_to` site emits a `let c := and(c, 0xff..ff_128)` +mask), and `OPTIMISATION.md` to document the security caveat. + +### G. Fold the keccak squeezes in cp10 +**Projection: 30–50 kg saved** + +Cp10 spends ~120 kg on 4 squeeze_to keccak calls (after the eval and +q_eval loops). Each keccak operates on ~1.6 KB of buffered input — +which is fine, but the buffer is rebuilt 4 times in a row from the +same prefix. If we can compute x1 || x2 || x3 || x4 from a single +domain-separated keccak invocation (as snark-verifier does with its +`MidnightEvmHash`), we save ~3 keccak calls × ~10 kg = ~30 kg. + +### H. Set-0 q_com staging streamlining +**Status: H1 + H2 applied (-73 kg measured); H3 still open.** + +Pre-H1/H2 fine-grained attribution showed set 0's m=33 q_com fold +consumed 329 kg (36.8 % of verifier). Three sub-attacks were +proposed: + +#### H1 — MCOPY per-commit point staging (applied, −25 kg) + +Each MSM pair was emitted as 4-mstore chain pulling the point from +the VK region into MSM_SCRATCH. EcPoint guarantees the 4 words sit +at `base..base+3*0x20` (contiguous), so `mcopy(stage, src, 0x80)` +covers the whole point copy in one op. Applied to (a) m≥2 per-commit +loop in block 3, (b) post-staticcall point writeback (MSM_SCRATCH → +q_com_base), and (c) m=1 short-circuit. Measured: cp19 −26 kg, cp20 +−0.1 kg, cp21 −0.1 kg. Production: 980 → 955 kg. + +#### H2 — Roll x1-powers emission into a Yul `for` loop (applied, −48 kg) + +Block 2 unrolled 32 mulmod+mstore pairs, costing 24-26 kg vs the +~700 gas the arithmetic itself requires. solc-via-ir's register +allocator struggles with 32 unrolled mulmods sharing one accumulator, +plus the unrolled basic block penalizes block 3's downstream +register allocation. Replacing with: + +```yul +let acc := 1 +let p := X1_POWERS_MPTR +for { let i := 0 } lt(i, 0x20) { i := add(i, 1) } { + p := add(p, 0x20) + acc := mulmod(acc, x1, r) + mstore(p, acc) +} +``` + +restores the basic-block heuristic. Measured: cp18 −22 kg directly, +cp19 −26 kg as downstream re-scheduling effect, plus minor wins +elsewhere. Production: 955 → 907 kg. + +The downstream effect on cp19 is the surprising win — solc-via-ir +reschedules the whole PCS section more compactly when block 2 is no +longer fighting for register slots. + +#### H3 (open) — Pre-reverse calldata evals to a Fr-array +**Projection: −10 to −20 kg** + +The q_eval accumulator inside each set's q_com fold reads each +commit's eval via `byte_reverse_32(calldataload(N))`. For set 0 +that's 33 calls × ~145 gas = ~4.8 kg. Same evals are also read +~150-300 times in the gate evaluator (cp12 = 124 kg). Pre-reversing +all evals once at top of PCS block 0 into a contiguous Fr-array: + +```yul +mstore(EVALS_REVERSED_MPTR + 0x00, byte_reverse_32(calldataload(EVAL_OFFSET_0))) +mstore(EVALS_REVERSED_MPTR + 0x20, byte_reverse_32(calldataload(EVAL_OFFSET_1))) +... +``` + +then replacing every later use with `mload(EVALS_REVERSED_MPTR + +i*0x20)`. Saves byte_reverse_32 cost on 100-200 references × 142 gas += 14-28 kg, minus ~7 kg setup. Net ~7-20 kg. + +**Risk**: requires plumbing through both the PCS emitter (so it +substitutes mloads for the calldata pattern) AND the gate evaluator +(so cp12 also gets the savings). Need to coordinate offsets across +the two emitters. + +**Caveat:** changes the Fiat-Shamir transcript layout. Requires a +matching change to the prover (in `midnight-proofs::CircuitTranscript`) +and bumps the on-chain transcript's domain-separator epoch. Not as +cheap as it looks — cross-stack coordination. + +**Files:** `midfall/proofs/src/transcript/mod.rs`, +`src/transcript.rs`, `templates/contracts/Halo2Verifier.sol`. Probably a +follow-up after A is shipped. + +## Realistic projection after Step 6 + +| step | description | gas | delta | +|---|---|---:|---:| +| Step 5 (pre-unroll) | with instrumentation overhead | 1,488 kg | — | +| Step 5 (production) | instrumentation off | 1,476 kg | −12 kg | +| Step 6 (production) | byte_reverse_32 unrolled | 997 kg | −478 kg | +| Step 6 (instrumented) | with checkpoints overhead | 1,010 kg | — | +| Step 6 + B (production) | Montgomery batch scalar inv | 982 kg | −15 kg | +| Step 6 + B + E (production) | + MCOPY EC point staging | 981 kg | −1.5 kg | +| Step 6 + B + D + E (production) | + hoist rotation mloads | 980 kg | −0.6 kg | +| Step 6 + B + D + E + H1 (production) | + MCOPY q_com per-commit staging | 955 kg | −25 kg | +| **Step 6 + B + D + E + H1 + H2 (HEAD, production)** | **+ rolled x1-powers loop** | **907 kg** | **−48 kg** | +| Step 6 + B+D+E+H1+H2 (instrumented) | with checkpoints overhead | 924 kg | — | +| + C | constant-fold `mulmod(_, 1)` / `addmod(_, 0)` | ~895 kg | −10 kg | +| + F (opt-in) | `truncated-challenges` (128-bit) | ~770 kg | −135 kg | +| + G (cross-stack) | fold consecutive keccak squeezes | ~740 kg | −30 kg | + +**Achieved non-opt-in: 907 kg (−581 kg, −36.6 % from baseline).** The +remaining ~900-kg floor is dominated by: + +| component | est gas | +|---|---:| +| EIP-2537 G1MSMs (53 pairs across 9 calls) | ~225 kg | +| EIP-2537 G1ADDs (5 calls) | ~2 kg | +| EIP-2537 PAIRING (2-pair) | ~108 kg | +| modexp (1 batched scalar_inv after B) | ~2 kg | +| Keccak transcript (3 absorb + 8 squeeze) | ~25 kg | +| Tx base + calldata + extcodecopy | ~85 kg | +| EVM arithmetic (gate eval + Lagrange + interp) | ~150 kg | +| EVM helper dispatch + memory traffic | ~150 kg | +| Other (control flow, etc.) | ~200 kg | + +Below ~950 kg the optimisation surface narrows to soundness-relevant +trade-offs (F) and cross-stack transcript redesign (G), and beyond that +the floor is dominated by EIP-2537 pricing and the cryptographic work. + +## Notes + +- The instrumentation is purely additive: with `--features + solidity-gas-checkpoints` off, the rendered verifier is byte-for-byte + the production output. The default test currently confirms **907,030 + gas** (post-Step-6 + B + D + E + H1 + H2); the 17.25-kg overhead + seen with checkpoints on is exactly `23 × 750` (16 top-level + + 7 PCS sub-block checkpoints). +- `SOLC_OPTIMIZE_RUNS=N` overrides `compile_solidity`'s default at run + time. `DEFAULT_OPTIMIZE_RUNS = 200`. An A/B sweep showed the gas + number is essentially flat across `runs ∈ {1, 50, 200, 1000, + 100000}` (only ~2 kg spread), so `200` was chosen for hygiene + rather than measurable savings. +- The poseidon fixture has `HAS_ACCUMULATOR_MPTR == 0` so cp15 + measures the no-op branch (~39 gas of `mload(HAS_ACCUMULATOR_MPTR) + + iszero + jumpi`). Aggregator deployments will see real cost here + — re-run the bench against an aggregated proof to populate cp15. +- Section attributions are *causal*: the LOG1 sits exactly at the end + of each block, so the delta is unambiguously the gas spent in that + section (modulo the 750-gas overhead per checkpoint, which is + subtracted by `dump_gas_checkpoints`). +- For tighter attribution within the 631 kg PCS block, add additional + checkpoints inside `src/codegen/pcs.rs::computations()` at the + per-set boundaries. Currently every set's three sub-stages (point + set group, batch invert, MSM) coalesce into the same 631 kg bucket. diff --git a/proofs/solidity-verifier/docs/benchmarks/FEWER_POINT_SETS_MSM_ANALYSIS.md b/proofs/solidity-verifier/docs/benchmarks/FEWER_POINT_SETS_MSM_ANALYSIS.md new file mode 100644 index 000000000..17fd90a65 --- /dev/null +++ b/proofs/solidity-verifier/docs/benchmarks/FEWER_POINT_SETS_MSM_ANALYSIS.md @@ -0,0 +1,88 @@ +# Fewer Point Sets MSM Analysis + +Short version: `fewer-point-sets` helps bytecode size, but it does **not** +reduce the big MSM in the current fused-MSM design. It actually makes gas worse +in this bench. + +I tried the real outer feature: + +```sh +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ +cargo test --release \ + --features evm,truncated-challenges,fewer-point-sets,solidity-gas-checkpoints \ + --test ivc_keccak_solidity ivc_final_keccak_solidity_e2e \ + -- --ignored --nocapture +``` + +It enabled the outer layout, but the Solidity verifier reverted: + +```text +outer proof fewer-point-sets: enabled +proof eval scalars total: 259 (main: 102, dummy PCS: 157) +verifier reverted at gas_used = 2,069,550 +``` + +Then I reran with `solidity-trace` so the final revert would not discard +checkpoint logs. That verifier returned `0`, so it is diagnostic only, but it +gives the section costs. + +## Key MSM Observation + +- Point sets dropped from `5` to `1`. +- Final fused MSM input stayed `65` pairs: generated staticcall input length was + `0x28a0 = 65 * 0xa0`. +- Baseline latest commit also used a `65`-pair final MSM. +- So `fewer-point-sets` does not reduce the main MSM pair count after our + fusion. It mostly trades point-set count for 157 dummy eval scalars. + +## Fewer-Point-Sets Diagnostic Bench + +```text +real section work: 1,833,230 +baseline latest real section work: 1,681,292 +delta: +151,938 gas +``` + +```text +id gas section +2 4,528 VK loading +3 12,247 VK digest + committed_pi + instance absorbs +4 7,392 user-phase advice reads + user challenge squeezes +5 3,320 theta squeeze + lookup multiplicities +6 5,290 beta/gamma + permutation Z products +7 1,126 lookup helpers + Z accumulators +8 2,591 trash_challenge + trashcans +9 2,985 y squeeze + quotient-limb reads +10 248,795 evaluations + x1/x2 + f_com + x3 + q_evals + x4 + pi +11 18,207 Lagrange + instance evaluation +12 633,286 batched identity numerator reconstruction +13 119,616 linearization-commitment MSM +17 271 PCS block 1 +18 8,528 PCS block 2 (x1 powers) +19 38,146 PCS block 3 set 0 (q_eval fold) +20 5,949 PCS block 4 (f_eval Lagrange interpolation) +21 452,539 PCS block 5 (final_com x4-power MSM + v) +14 25,435 PCS block 6 +15 140,561 public accumulator pairing check +16 103,168 final proof ec_pairing +``` + +Relevant deltas vs latest baseline: + +```text +evaluations/transcript: 107,867 -> 248,795 (+140,928) +PCS q_eval folds total: 11,092 -> 38,146 (+27,054) +PCS f_eval: 21,880 -> 5,949 (-15,931) +PCS final MSM: 458,782 -> 452,539 (-6,243) +``` + +Contract size does improve in the non-trace failed run: + +```text +verifier runtime: 12,614 -> 10,465 bytes +total runtime: 47,937 -> 45,788 bytes +``` + +But for gas, it is the wrong trade here. Since the final MSM is already fused +over distinct commitments, reducing point sets no longer removes MSM terms; it +just adds dummy openings and more transcript/eval work. diff --git a/proofs/solidity-verifier/docs/benchmarks/FEWER_POINT_SETS_NOTES.md b/proofs/solidity-verifier/docs/benchmarks/FEWER_POINT_SETS_NOTES.md new file mode 100644 index 000000000..bc3b2194b --- /dev/null +++ b/proofs/solidity-verifier/docs/benchmarks/FEWER_POINT_SETS_NOTES.md @@ -0,0 +1,233 @@ +# Fewer Point Sets And Contract Size + +## Summary + +Disabling `fewer-point-sets` for the outer Solidity-facing proof helps proof +size and calldata size, but it does not materially reduce smart contract size. + +The reason is that dummy queries mostly affect the PCS multi-open proof layout, +not the batched identity numerator reconstruction code. + +## What Improved + +With outer `fewer-point-sets` disabled, the final decider proof no longer carries +dummy PCS eval scalars: + +```text +proof eval scalars: 259 -> 102 +dummy PCS evals: 157 -> 0 +compressed proof: 9952 -> 5056 bytes +calldata: 13188 -> 8292 bytes +``` + +This is a real calldata/proof-size win. + +## Why Contract Size Did Not Shrink + +Verifier bytecode is dominated by generated verifier logic, especially the +batched identity numerator reconstruction: + +```text +gates + permutation identities + lookup identities + trash identities +``` + +Those identities are determined by the circuit and verifying key shape. They +still require the same advice, fixed, permutation, lookup, and trash evaluations +whether dummy PCS queries exist or not. + +Dummy queries do not create those identities. They only add artificial PCS +openings so multiple multi-open point sets can be merged. + +## Layout Tradeoff + +With dummy queries: + +```text +PCS point sets: fewer +proof eval scalars: more +calldata: larger +PCS verifier code/path: smaller/simpler +``` + +Without dummy queries: + +```text +PCS point sets: more +proof eval scalars: fewer +calldata: smaller +PCS verifier code/path: larger/more sections +``` + +In the IVC decider benchmark: + +```text +outer fewer-point-sets on: 1 PCS point set, 259 eval scalars +outer fewer-point-sets off: 5 PCS point sets, 102 eval scalars +``` + +So disabling dummy queries can slightly increase verifier bytecode, because the +PCS code has to handle more point-set folds. + +## MSM Effect After Fused Final Commitment + +After `fb6ff8c perf(evm): fuse PCS final commitment MSM`, the main PCS +commitment fold no longer materializes per-set `q_com` points. Instead it emits +one final MSM over all non-identity commitments: + +```text +final_com = sum_{s,i} (x4^s * x1^i) * C[s][i] + x4^n_sets * f_com +``` + +That changes the expected benefit of `fewer-point-sets`. The feature can reduce +the number of point sets, but it does not reduce the number of distinct +commitments in the fused final MSM. + +### Diagnostic Run + +Command: + +```sh +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ +cargo test --release \ + --features evm,truncated-challenges,fewer-point-sets,solidity-gas-checkpoints \ + --test ivc_keccak_solidity ivc_final_keccak_solidity_e2e \ + -- --ignored --nocapture +``` + +The normal run enabled the outer fewer-point-sets layout, but the generated +Solidity verifier reverted: + +```text +outer proof fewer-point-sets: enabled (dummy query evals expected) +proof eval scalars total: 259 (main: 102, dummy PCS: 157) +verifier reverted at gas_used = 2,069,550 +``` + +To observe the section costs despite the final pairing failure, the bench was +rerun with `solidity-trace`, which skips the final revert and returns the raw +`success` bit. That diagnostic run returned `0`, so it is useful only for gas +attribution, not as a passing verifier result. + +### Observed MSM Shape + +```text +outer fewer-point-sets off: 5 PCS point sets, 102 proof eval scalars +outer fewer-point-sets on: 1 PCS point set, 259 proof eval scalars +``` + +With fewer-point-sets enabled, the generated final MSM staticcall used: + +```text +input length = 0x28a0 = 65 * 0xa0 +``` + +That is still a 65-pair final MSM, the same pair count as the fused-MSM baseline. +The feature collapsed the PCS point sets, but it did not reduce the number of +commitments staged into the final MSM. + +### Detailed Diagnostic Breakdown + +Diagnostic command: + +```sh +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ +cargo test --release \ + --features evm,truncated-challenges,fewer-point-sets,solidity-gas-checkpoints,solidity-trace \ + --test ivc_keccak_solidity ivc_final_keccak_solidity_e2e \ + -- --ignored --nocapture +``` + +Results: + +```text +total tx gas_used: 2,108,542 +real section work: 1,833,230 +checkpoint overhead: 15,750 +verifier return value: 0 +``` + +Contract sizes: + +```text +Halo2Verifier runtime: 10,919 bytes +Halo2VerifyingKey runtime: 13,568 bytes +Halo2QuotientEvaluator runtime: 21,755 bytes +total runtime: 46,242 bytes +``` + +Section breakdown: + +```text +id gas section +2 4,528 VK loading +3 12,247 VK digest + committed_pi + instance absorbs +4 7,392 user-phase advice reads + user challenge squeezes +5 3,320 theta squeeze + lookup multiplicities +6 5,290 beta/gamma + permutation Z products +7 1,126 lookup helpers + Z accumulators +8 2,591 trash_challenge + trashcans +9 2,985 y squeeze + quotient-limb reads +10 248,795 evaluations + x1/x2 + f_com + x3 + q_evals + x4 + pi +11 18,207 Lagrange + instance evaluation +12 633,286 batched identity numerator reconstruction +13 119,616 linearization-commitment MSM +17 271 PCS block 1 (rotation points) +18 8,528 PCS block 2 (x1 powers) +19 38,146 PCS block 3 set 0 (q_eval fold) +20 5,949 PCS block 4 (f_eval Lagrange interpolation) +21 452,539 PCS block 5 (final_com x4-power MSM + v) +14 25,435 PCS block 6 (pairing inputs) +15 140,561 public accumulator pairing check +16 103,168 final proof ec_pairing +``` + +Relevant deltas versus the fused-MSM baseline with outer fewer-point-sets +disabled: + +```text +real section work: 1,681,292 -> 1,833,230 (+151,938) +evaluations/transcript: 107,867 -> 248,795 (+140,928) +PCS q_eval folds total: 11,092 -> 38,146 (+27,054) +PCS f_eval: 21,880 -> 5,949 (-15,931) +PCS final MSM: 458,782 -> 452,539 (-6,243) +``` + +### Takeaway For MSMs + +`fewer-point-sets` is not a useful gas optimisation for the current fused-MSM +verifier. It reduces point-set count and shrinks verifier bytecode, but the +large final commitment MSM remains a 65-pair MSM. The extra 157 dummy evals +increase transcript/evaluation work enough to dominate the small PCS savings. + +## VM vs Non-VM Identity Reconstruction + +The quotient/identity representation has a much larger effect on contract size: + +```text +compact VM path: small verifier bytecode, high identity gas +non-VM inline CSE: large verifier bytecode, low identity gas +``` + +Observed non-VM inline CSE run with outer dummy queries disabled: + +```text +batched identity numerator reconstruction: 121,625 gas +total tx gas: 1,458,659 +Halo2Verifier runtime: 60,043 bytes +VK runtime: 6,752 bytes +total runtime: 66,795 bytes +``` + +This confirms the main contract-size bottleneck is the inlined identity +arithmetic, not dummy PCS eval scalars. + +## Practical Takeaway + +Use outer `fewer-point-sets` disabled when optimizing proof size and calldata. + +Use the compact VM or a sharded/helper-contract identity evaluator when targeting +the 24 KB contract-size limit. + +Use non-VM inline CSE when measuring the lower bound for identity reconstruction +gas, accepting that it is not deployable as a single contract under the 24 KB +limit. diff --git a/proofs/solidity-verifier/docs/benchmarks/IVC_LATEST_BENCH_RESULTS.md b/proofs/solidity-verifier/docs/benchmarks/IVC_LATEST_BENCH_RESULTS.md new file mode 100644 index 000000000..1a8ff39a9 --- /dev/null +++ b/proofs/solidity-verifier/docs/benchmarks/IVC_LATEST_BENCH_RESULTS.md @@ -0,0 +1,143 @@ +# Latest IVC Keccak Solidity Bench Results + +## Decider Outer Proof Without Fewer Point Sets + +Command: + +```sh +scripts/run_ivc_bench.sh --skip-srs-download +``` + +Result: PASS. The final Keccak IVC proof for two one-step Poseidon hash-chain +leaves was accepted on-chain. This run keeps the recursive in-circuit verifier +on the fewer-point-sets layout, but scopes the outer decider proof to the +non-fewer layout. + +Feature shape: + +```text +evm,truncated-challenges,in-circuit-fewer-point-sets,solidity-gas-checkpoints +``` + +### Summary + +```text +total tx gas: 1,676,195 +real checkpointed work: 1,512,643 +checkpoint overhead: 19,500 +verifier runtime: 16,463 bytes +VK runtime: 15,136 bytes +quotient runtime: 18,318 bytes +total deployed runtime: 49,917 bytes +proof compressed: 5,056 bytes +proof padded: 7,776 bytes +calldata: 8,356 bytes +public inputs: 14 field elements +``` + +### Evaluation Counts + +```text +proof eval scalars total: 102 (main: 102, dummy PCS: 0) +instances: 1 proof eval from committed instance queries, 1 public-input eval computed locally +advice evals: 40 +fixed evals: 17 proof evals, 10 simple-selector fixed columns omitted +permutation evals: 35 total (18 common/sigma, 17 product/Z across 6 sets) +lookup evals: 8 total (2 multiplicity, 2 helper, 4 accumulator z/z_next) +trash evals: 1 +PCS point sets: 5 +``` + +### Detailed Gas Checkpoints + +| id | gas | % | section | +| ---: | ---: | ---: | --- | +| 2 | 5,037 | 0.3% | VK loading | +| 3 | 1,801 | 0.1% | VK digest + committed_pi + instance absorbs | +| 4 | 9,096 | 0.6% | user-phase advice reads + user challenge squeezes | +| 5 | 1,559 | 0.1% | theta squeeze + lookup multiplicities | +| 6 | 3,090 | 0.2% | beta/gamma + permutation Z products | +| 7 | 1,889 | 0.1% | lookup helpers + Z accumulators | +| 8 | 860 | 0.1% | trash_challenge + trashcans | +| 9 | 2,031 | 0.1% | y squeeze + quotient-limb reads | +| 10 | 18,553 | 1.2% | evaluations + x1/x2 + f_com + x3 + q_evals + x4 + pi | +| 11 | 19,952 | 1.3% | Lagrange + instance evaluation | +| 12 | 705,271 | 46.6% | batched identity numerator reconstruction | +| 13 | 2,828 | 0.2% | linearization scalar prep | +| 17 | 215 | 0.0% | PCS block 1, rotation points x*omega^rot | +| 18 | 5,621 | 0.4% | PCS block 2, x1 powers | +| 19 | 6,650 | 0.4% | PCS block 3 set 0, q_eval fold | +| 20 | 241 | 0.0% | PCS block 3 set 1, q_eval fold | +| 21 | 185 | 0.0% | PCS block 3 set 2, q_eval fold | +| 22 | 2,863 | 0.2% | PCS block 3 set 3, q_eval fold | +| 23 | 1,219 | 0.1% | PCS block 3 set 4, q_eval fold | +| 24 | 5,108 | 0.3% | PCS block 3, q_com input materialization | +| 25 | 18,349 | 1.2% | PCS block 4, f_eval Lagrange interpolation | +| 26 | 531,289 | 35.1% | PCS block 5, final_com x4-power MSM + v | +| 14 | 25,596 | 1.7% | PCS block 6, pairing inputs LHS/RHS | +| 15 | 40,822 | 2.7% | public accumulator pairing batch prep | +| 16 | 103,268 | 6.8% | final proof ec_pairing | + +### Contract Sizes + +```text +solc optimize runs: 1 +solc CBOR metadata: omitted +Halo2Verifier.sol source bytes: 112,731 +Halo2VerifyingKey.sol source bytes: 57,937 +Halo2QuotientEvaluator.sol source bytes: 137,130 +Halo2Verifier creation bytecode bytes: 17,020 +Halo2VerifyingKey creation bytecode bytes: 14,832 +Halo2QuotientEvaluator creation bytes: 18,344 +Halo2Verifier deployed runtime bytes: 16,463 +Halo2VerifyingKey deployed runtime bytes: 15,136 +Halo2QuotientEvaluator runtime bytes: 18,318 +total deployed runtime bytes: 49,917 +``` + +### Main Remaining Gas Targets + +```text +batched identity numerator reconstruction: 705,271 gas +PCS final fused MSM: 531,289 gas +final proof ec_pairing: 103,268 gas +public accumulator pairing batch prep: 40,822 gas +Lagrange + instance evaluation: 19,952 gas +``` + +## Default Outer Fewer-Point-Sets Comparison + +Command: + +```sh +scripts/run_ivc_bench.sh --skip-srs-download +``` + +Result: PASS. In this default run both the recursive verifier and the outer +decider proof use fewer point sets. + +```text +total tx gas: 1,793,588 +real checkpointed work: 1,556,056 +checkpoint overhead: 16,500 +verifier runtime: 13,817 bytes +VK runtime: 15,136 bytes +quotient runtime: 18,318 bytes +total deployed runtime: 47,271 bytes +proof compressed: 9,952 bytes +proof padded: 12,672 bytes +calldata: 13,252 bytes +public inputs: 14 field elements +proof eval scalars total: 259 (main: 102, dummy PCS: 157) +PCS point sets: 1 +``` + +Largest default sections: + +```text +batched identity numerator reconstruction: 708,897 gas +PCS final fused MSM: 529,034 gas +final proof ec_pairing: 103,268 gas +evaluations/transcript tail: 43,294 gas +public accumulator pairing batch prep: 40,822 gas +``` diff --git a/proofs/solidity-verifier/docs/benchmarks/OPTIMISATION.md b/proofs/solidity-verifier/docs/benchmarks/OPTIMISATION.md new file mode 100644 index 000000000..5f79c6c78 --- /dev/null +++ b/proofs/solidity-verifier/docs/benchmarks/OPTIMISATION.md @@ -0,0 +1,273 @@ +# Halo2 BLS12-381 Solidity Verifier — Optimisation Plan + +Baseline (Poseidon fixture, k=6, ZkStdLib, midnight-proofs multi-prepare PCS, +3 point sets, 19 G1 commitments + f_com + pi): + +``` +Poseidon proof verified on-chain in 1 589 076 gas +``` + +Most of that 1.59 M is sitting in the way the verifier issues **single-pair +G1MSMs in a Horner loop**. Below is a ranked list of optimisations with rough +gas estimates. + +## EIP-2537 G1MSM cost reminder + +``` +single-pair 22 500 +2-pair 28 500 (vs 45 000 if split) +4-pair 40 500 (vs 90 000) +8-pair 64 500 (vs 180 000) +16-pair 112 500 (vs 360 000) +``` + +The same fold done as one k-pair MSM is roughly half the gas of the same +fold done as k single-pair MSMs. + +--- + +## 1. Batch every Horner fold into a single multi-pair G1MSM +**Biggest win: ~500-600 kg saved** + +The current emitter does this everywhere: + +```yul +ec_mul_acc(success, scalar) // 22 500 +mstore(0x180, ...) // stage next point at TMP +ec_add_acc(success) // 600 +``` + +That's 44 × 22 500 = ~990 kg today. The exact same computation fits a +single G1MSM call per logical fold: + +| fold | pairs | now | batched | saved | +|---|---|---|---|---| +| quotient limbs (4) | 4 | 92 kg | 40 kg | 52 kg | +| simple-selector adds | 4 | 92 kg | 40 kg | 52 kg | +| PCS multi-prepare set #1 | ~12 | 270 kg | ~95 kg | 175 kg | +| PCS multi-prepare set #2 | ~10 | 225 kg | ~85 kg | 140 kg | +| PCS multi-prepare set #3 | ~8 | 180 kg | ~70 kg | 110 kg | +| random-combine LHS/RHS | 2 × 1 | 45 kg | 2 × 22.5 kg | 0 | +| **subtotal** | | | | **~530 kg** | + +Concretely: replace `ec_mul_acc` / `ec_add_acc` chains with a helper that +lays out `[(scalar, point), …]` contiguously in memory and issues one +staticcall at `0x0c`. Pre-compute the Horner scalars in Fr first +(each scalar is one `mulmod`, ~5 gas), then dispatch. + +This is the highest-leverage change and is contained inside the Yul +emitter — the Horner loop turns into a scalar-array build + one +staticcall. + +## 2. Fold `(1 − xâ¿)` into the pre-computed scalars +**~22 kg saved** + +Today there's a standalone `ec_mul_acc(success, one_minus_x_n)` call. +That's a full 22.5 kg G1MSM just to apply a Fr factor. Multiply it into +every quotient/selector scalar before staging and drop the call. + +## 3. Combine `random_combine` LHS + RHS into one MSM +**~16 kg saved** + +`ACC_LHS, PAIRING_LHS` (and the RHS pair) can be folded with the random +challenge as one 2-pair MSM each, instead of `mul + add` × 2. + +## 4. Pre-compute scalars off-chain if the verifier accepts hints +**~50 kg saved** + +The Horner scalars `[1, x, x², …, xáµ]` are deterministic from the public +challenges. They could be passed as calldata hints (with an on-chain +consistency check via one or two `mulmod`s plus a final `addmod` of +`Σ scalaráµ¢ · xâ±` against a known marker). This trades ~50 calldata words +(~1.6 kg) for ~50 kg of avoided `mulmod` chains. Only worth doing if the +surrounding PCS layout supports it. + +## 5. MCOPY the G2 / scratch staging +**Status: applied (Step 8/E). Saved ~1.5 kg measured.** + +`ec_pairing` had two `for i in 0..8 { mstore(...) mload(...) }` loops to +copy the two G2 points into the precompile input scratch, plus two +4-line G1 mstore chains. Cancun ships MCOPY (`0x5e`); each loop is now +one `mcopy(dst, src, 0x100)` and each G1 chain is `mcopy(dst, src, +0x80)`. Same trick applied to PCS Block 5/6 EC-point staging +(q_com seed, F_COM, FINAL_COM, PAIRING_LHS/RHS, G1_BASE, PI). + +Measured saving was lower than the original 5–10 kg projection (1,499 +gas) because solc-via-ir was already folding many of the +mstore-then-mload pairs into stack locals; the visible win comes from +the patterns where the round-trip crossed a precompile call boundary +(which solc cannot fold). + +## 6. Drop pairs whose scalar is zero +**0-30 kg, depends on circuit** + +If any precomputed Horner scalar is zero (shouldn't be for honest proofs +but happens after the 1-xâ¿ fold trick), skip the pair. EIP-2537 charges +per pair, so each skipped pair saves 12 kg. + +## 7. Bump `--optimize-runs` back up +**~30-60 kg saved (runtime)** + +`compile_solidity` forces `--optimize-runs=1` so 21+ inline `decompress_g1` +helpers don't blow EIP-170 (24 kB). If we move `decompress_g1` into a +tiny *library contract* and `delegatecall` to it, we can: + +- ship the verifier with `--optimize-runs=200` (or even default 200), + saving the per-call inlining penalty, +- amortize `decompress_g1` across all 21 sites. + +Tradeoff: 13 `delegatecall`s (~700 each = ~10 kg back) vs ~30-60 kg +runtime savings. Net positive. + +## 8. Reuse PCS scratch slots between sets +**~5-15 kg saved** + +The 44 + +```yul +mstore(0x180, …) +mstore(0x1a0, …) +mstore(0x1c0, …) +mstore(0x1e0, …) +``` + +blocks pay for redundant memory expansion. Once we batch into one MSM +(#1), the staging area becomes a single large strip and the per-pair +`mstore` chain disappears entirely. + +--- + +## Realistic projection + +| variant | gas | +|---|---| +| today | 1 590 kg | +| + #1 (batched MSMs) | ~1 060 kg | +| + #2 (fold 1-xâ¿) | ~1 040 kg | +| + #3 (random-combine batched) | ~1 020 kg | +| + #5 (MCOPY G2 + staging) | ~1 010 kg | +| + #7 (decompress_g1 library, optimize-runs=200) | ~960 kg | + +A **~600 kg reduction (~38%) is realistic** without touching the math, +just by batching MSMs and tightening memory copies. That puts the +verifier in the ~960 kg territory, which is roughly the EIP-2537 floor +for this circuit shape (3 point sets × ~10 commits + 3-pair pairing). + +The single highest-leverage change is **#1 (batch G1MSMs)** — a contained +codegen-side change in the Yul emitter (the Horner loop becomes a +scalar-array build + one staticcall). + +--- + +## Realised savings (Poseidon fixture, midfall branch) + +| step | description | gas after | delta | cumulative | +|---|---|---:|---:|---:| +| 0 | baseline (single-pair Horner everywhere) | 1 589 076 | — | — | +| 1 | quotient fold + (1−xâ¿) + simple-selector adds → one (k+n_sel)-pair MSM (template-only) | 1 558 817 | −30 259 | −30 259 | +| 2 | PCS Block 3 per-set q_com fold → per-set m-pair MSM at MSM_SCRATCH=0x6100 (codegen) | 1 482 958 | −75 859 | −106 118 | +| 3 | PCS Block 5 final_com → (n_sets+1)-pair MSM | 1 488 783 | +5 825 | reverted | +| 4 | PCS Block 6 PAIRING_RHS → 3-pair MSM | (above included) | +0 | reverted | +| 5 | drop on-the-fly compressed encoding: hash uncompressed 128B verbatim in transcript (patch midnight-proofs `Hashable::to_input` + EVM `common_uncompressed_g1`) | 1 475 536 | −7 422 | −113 540 | +| 6 | unroll `byte_reverse_32` (31 of 32 iterations straight-line + 1-trip guard loop). Each of the ~184 call sites drops from ~700 gas (32-iter shift loop body) to ~140 gas (32 byte-extract + or-shl ops). The trailing 1-trip loop is required to keep solc from inlining the entire 32-step body at every call site under `--via-ir`; full inlining triggers a pathology where the verifier consumes the full block gas limit. | 997 438 | −478 098 | −591 638 | +| 7 (B) | PCS Block 4 Lagrange interpolation: replace n separate `scalar_inv` calls per point set with one Montgomery batch invert (`{dx_j, lbasis_j}` for j=0..m, n=2m). Per set: 1 modexp + 3n−3 muls vs n modexp; `den_inv` becomes a free `prod_j dx_inv_j`. Soundness: dx_j non-zero by Fiat-Shamir, lbasis_j non-zero by `construct_intermediate_sets` de-dup. | 982 229 | −15 173 | −606 811 | +| 8 (E) | MCOPY EC-point staging: replace 4-line `mstore(N, mload(M))` chains and 8-iter G2 mstore loops with `mcopy(dst, src, 0x80)` / `mcopy(dst, src, 0x100)` calls in `ec_pairing` (G2_BASE + NEG_S_G2_BASE + 2 G1 inputs) and PCS Blocks 5 & 6 (q_com seed, F_COM staging, FINAL_COM persist, PAIRING_LHS/RHS staging). | 980 730 | −1 499 | −608 310 | +| 9 (D) | hoist `mload(add(ROT_POINTS_MPTR, k*0x20))` to `rot_pt_i` stack locals at the top of the f_eval block. Each rotation point is referenced O(m²) times per set across `dx_j` and `lbasis_j`; solc-via-ir cannot CSE-fold across the inline `scalar_inv` precompile boundary. | 980 125 | −605 | −608 951 | +| 10 (H1) | MCOPY q_com fold per-commit point staging in PCS block 3 (replaces 4-mstore chains for each commit's 4-word point with `mcopy(stage, src, 0x80)`). Hits all 40 commits across 3 sets (m=33+5+2) plus the m=1 short-circuit and the post-staticcall MSM result writeback. | 955 224 | −24 901 | −633 852 | +| 11 (H2) | Roll PCS block 2 (x1 powers) emission from 32 unrolled `mulmod+mstore` pairs into a Yul `for` loop. Direct saving cp18 −22 kg + downstream cp19 re-scheduling −26 kg from solc-via-ir reallocating registers across the (now smaller) basic block. | 907 030 | −48 194 | −682 046 | + +**Net result: 1 589 076 → 907 030 gas (−682 046, −42.9%) on the Poseidon +fixture.** With current per-section breakdown: PCS 441 kg (49%), pairing +103 kg (11%), quotient eval 124 kg (14%), linearization MSM 72 kg (8%), +transcript+evals 80 kg (9%), other 87 kg (10%). + +Findings: + +- **Step 1** (template) absorbed projections #1 (quotient fold), #2 + (1−xâ¿ fold), and the simple-selector ec_add_acc chain into a single + staticcall. Saved 30 kg as predicted. +- **Step 2** (codegen, PCS Block 3) is where the bulk of the saving + lives: set 0 alone has 33 commits, and 33 single-pair G1MSMs collapse + into one 33-pair MSM. The first attempt staged at 0x00 and clobbered + the VK region (VK_MPTR=0x0ee0..0x2040, set 0 needs 0x14a0 bytes). + Relocating the staging to MSM_SCRATCH=0x6100 (above scalar_inv's + 0x6000..0x60c0 scratch) fixed it. +- **Steps 3 & 4 reverted**: for n_sets=3 (Poseidon), the 4-pair and + 3-pair MSMs cost more than the single-pair chain they replace once + staging mstore overhead is counted. The break-even is around 5 pairs + for these blocks. The current chain-style code seeds the accumulator + with q_com[0]/final_com directly (no MSM call), which makes the + effective comparison `(n-1) × 22.5 kg + (n-1) × 0.5 kg` vs + `n-pair MSM + 4 × mstore_seed`. +- The projection table assumed all batching was uniformly beneficial; + in practice multi-pair MSM only beats the single-pair chain at + pair-count ≥ 4 with a non-trivial seed. + +### Where the remaining gas lives + +After Step 5 the bottleneck shifted to: + +- `decompress_g1` calls (still ~13 sites, ~80 kg total per audit) +- `scalar_inv` Fermat-style ladder (~30 kg per call, multiple sites) +- The 3 G1MSM-1 + G1ADD chain in Block 5 (final_com fold, ~75 kg) +- Pairing precompile itself (~120 kg, can't be reduced) +- ~184 `byte_reverse_32` 32-iteration loops embedded in calldata reads + for advice, fixed, instance, and Q_EVAL (Step 6 attacked this). + +The next-highest-leverage item was originally projected to be **#7 +(decompress_g1 library + --optimize-runs=200)** at 30–60 kg, but +investigation showed: + +- `decompress_g1` was already removed in Step 5 (the on-the-fly compressed + encoding is gone). +- A full sweep of `--optimize-runs ∈ {1, 50, 200, 1000, 100000}` showed + only ~2 kg sensitivity — `--via-ir` already optimises aggressively + regardless of the runs setting. + +The actual top item turned out to be **byte_reverse_32 itself** (Step 6), +which was emitted unconditionally for every Calldata-backed Word reference +and ran a 32-iteration shift loop at each call site (~700 gas/call × +~184 calls = ~129 kg observed plus secondary effects). Unrolling 31 of +the 32 iterations into straight-line code dropped the per-call cost to +~140 gas and saved 478 kg — far above the 30–60 kg projection for +the now-irrelevant Item #7. + +--- + +## Cross-cutting change: uncompressed transcript hashing (Step 5) + +**Status:** applied. Saves ~7.5 kg on the Poseidon fixture. + +The previous emitter hashed each G1 commitment in its 48-byte ZCash +*compressed* encoding into the keccak transcript, computing the sign +bit on the fly via a 384-bit `lex(y) > lex(p − y)` ladder. We +transitioned both the prover (in `midnight-proofs`'s +`Hashable for G1Projective::to_input`) and the EVM verifier +(`common_uncompressed_g1`) to hash the **uncompressed 128-byte +EIP-2537 padded form** verbatim (`x_hi || x_lo || y_hi || y_lo`, +each coord = 16 zero pad bytes + 48 BE bytes of the field element). + +Wire format unchanged: proofs still carry G1 in the 48-byte compressed +encoding (`Hashable::to_bytes` / `read` are unmodified). The off-chain +`repack` step still decompresses to the 128-byte calldata form for the +EVM precompiles, just as before. + +Transcript malleability is prevented by masking the 16-byte zero pad +in `_hi` words before keccak absorbtion (defeats grinding attacks +that submit non-canonical pad bytes which the EIP-2537 precompile +would only reject later in the verifier). + +Files touched: + +- `vendor/.../midfall/proofs/src/transcript/implementors.rs` — + `Hashable for G1Projective::to_input` returns 128 bytes + instead of `::to_bytes` (compressed). +- `src/transcript.rs::common_g1` — absorbs the same 128 bytes; the + `common_g1_then_squeeze_matches` round-trip test pins this to + `CircuitTranscript`'s output. +- `templates/contracts/Halo2Verifier.sol::common_uncompressed_g1` — replaced a + ~50-line sign-bit ladder with a `mstore8 + calldatacopy(0x80) + 2 × + mask` sequence. +- The committed-instance identity injection at the start of the + transcript was rewritten from "0xc0 || 47*0x00" to "128 × 0x00" to + match the new identity convention. diff --git a/proofs/solidity-verifier/docs/benchmarks/PCS_BLOCK5_FINAL_MSM_ANALYSIS.md b/proofs/solidity-verifier/docs/benchmarks/PCS_BLOCK5_FINAL_MSM_ANALYSIS.md new file mode 100644 index 000000000..8b1c23b75 --- /dev/null +++ b/proofs/solidity-verifier/docs/benchmarks/PCS_BLOCK5_FINAL_MSM_ANALYSIS.md @@ -0,0 +1,187 @@ +# PCS Block 5 Final MSM Analysis + +Checkpoint: + +```text +537,061 gas, 35.7%, PCS block 5, final_com x4-power MSM + v +``` + +The exact gas has moved slightly between benchmark runs, but this section +remains the dominant PCS cost. After the big-endian proof-scalar shim it was +still around `533k` gas, so the remaining cost is not scalar decoding. + +## What This Step Does + +This block is the GWC/KZG multi-opening step that folds all point-set openings +into one final commitment opening. + +Earlier PCS blocks have already: + +- grouped queried commitments by rotation point, +- folded each point set by `x1` into scalar-side `q_eval` values, +- read the prover's `f_com`, +- computed the interpolation correction `f_eval` at `x3`. + +PCS block 5 samples or uses `x4` and computes: + +```text +final_com = sum_s x4^s * q_com[s] + x4^n_sets * f_com +v = sum_s x4^s * q_eval_at_x3[s] + x4^n_sets * f_eval +``` + +The Solidity generator does not materialize each intermediate `q_com[s]`. +Instead, it expands them by linearity: + +```text +q_com[s] = sum_i x1^i * C[s][i] +``` + +so the final commitment is built directly as: + +```text +final_com = + sum_{s,i} (x4^s * x1^i) * C[s][i] + + x4^n_sets * f_com +``` + +The synthetic linearization commitment is also expanded inside this same final +MSM. Its quotient-limb commitments and simple-selector commitments are staged as +ordinary MSM terms with the corresponding folded scalar. + +PCS block 6 then uses this block's `final_com` and `v` to build the final KZG +pairing equation, roughly: + +```text +e(final_com - v*G + x3*pi, [1]_2) = e(pi, [s]_2) +``` + +## Rust Verifier Correspondence + +This corresponds to the `x4` batching stage in: + +```text +/Users/Julien.Coolen/midfall/proofs/src/poly/kzg/mod.rs +``` + +around the verifier `multi_open` logic: + +```rust +let x4 = transcript.squeeze_challenge_scalar(); +let final_com = inner_product([q_coms..., f_com], powers(x4)); +let v = inner_product([q_evals..., f_eval], powers(x4)); +``` + +The Solidity codegen for this is in: + +```text +src/codegen/pcs.rs +``` + +around the generated `build final_com and v` block. + +The latest generated verifier dump showed: + +```yul +staticcall(gas(), 0x0c, 0x8e60, 0x30c0, 0x8e60, 0x80) +``` + +`0x30c0 / 0xa0 = 78`, so this block is currently doing a 78-pair BLS12-381 +G1MSM. + +## Why It Is Expensive + +The cost is dominated by the EIP-2537 G1MSM precompile over those commitment +pairs. + +The surrounding scalar arithmetic is comparatively small: + +- computing `x4` powers, +- computing `v`, +- multiplying by `x1` and `x4` powers, +- staging G1/scalar pairs in memory. + +So this is primarily a final MSM pair-count problem, not a byte-level Yul +micro-optimization problem. + +## Optimisation Options + +### 1. Reduce Final MSM Pair Count + +This is the main lever. + +Every queried commitment that survives into `q_com` becomes a term in this +final MSM. The linearization commitment expansion also contributes quotient-limb +and simple-selector terms. + +Useful targets: + +- fewer queried commitments, +- fewer simple-selector commitments, +- fewer quotient limb commitments, +- fewer rotation point-set entries, +- circuit/proof-layout changes that remove commitments from the PCS query set. + +This is the highest-impact path, but it is also the most protocol- and +circuit-aware. + +### 2. Add Pair-Category Instrumentation + +Before changing the proof layout, the generator should report the final MSM term +breakdown: + +```text +normal queried commitments +linearization quotient limbs +linearization simple selectors +f_com +terms per point set +total pair count +precompile input bytes +``` + +That would identify which bucket contributes most of the 78 pairs and make the +next optimisation measurable instead of speculative. + +### 3. Pre-Aggregate Duplicate Bases + +If the same commitment appears more than once in the final MSM, it can be +combined safely: + +```text +a*C + b*C = (a + b)*C +``` + +A quick check of the generated dump did not show obvious duplicate direct base +addresses among the parsed pairs, so this is likely low yield for the current +IVC verifier. It is still a useful generator invariant for other circuits. + +### 4. A/B Test Materializing Linearization First + +The current fused design is usually better because it avoids a separate +linearization MSM precompile. However, the precompile pricing is not perfectly +linear, so it is worth testing an env-flagged alternative: + +```text +1. materialize LINEARIZATION_COM with a smaller MSM, +2. include LINEARIZATION_COM as one pair in the final MSM. +``` + +This may or may not beat the current fully fused path. It should be benchmarked +rather than assumed. + +### 5. Avoid `fewer-point-sets` for This Cost Center + +The real `fewer-point-sets` feature was tested earlier. It improved bytecode +size, but it did not reduce the main fused final MSM pair count in this design +and made gas worse by adding dummy scalar/evaluation work. + +## Recommended Next Step + +Add final-MSM term-category diagnostics to the code generator and benchmark +output. Once the 78 pairs are broken down by source, target the largest bucket. + +The likely best follow-up is either: + +- reducing simple-selector or queried-commitment count, if those dominate, or +- A/B testing materialized linearization if the linearization expansion is a +large share of the final MSM. diff --git a/proofs/solidity-verifier/docs/benchmarks/PCS_FUSED_MSM_OPTIMISATIONS.md b/proofs/solidity-verifier/docs/benchmarks/PCS_FUSED_MSM_OPTIMISATIONS.md new file mode 100644 index 000000000..ca1fc3248 --- /dev/null +++ b/proofs/solidity-verifier/docs/benchmarks/PCS_FUSED_MSM_OPTIMISATIONS.md @@ -0,0 +1,161 @@ +# PCS Fused MSM Optimisations + +Latest commit documented: `fb6ff8c perf(evm): fuse PCS final commitment MSM` + +This note records the PCS optimisations used by the latest commit so the +reasoning is visible outside the git commit message. + +## Context + +The GWC19 multi-prepare verifier builds intermediate point sets and then folds +commitments and evaluations with transcript challenges: + +```text +q_com[s] = sum_i x1^i * C[s][i] +q_eval_set[s] = sum_i x1^i * eval(C[s][i]) +final_com = sum_s x4^s * q_com[s] + x4^n_sets * f_com +v = sum_s x4^s * q_eval[s] + x4^n_sets * f_eval +``` + +Before this change, generated Yul materialised each `q_com[s]` with a per-set +G1MSM and then folded those points again with `x4` powers. That paid for +intermediate commitment work even though the intermediate `q_com` points are not +needed after the final fold. + +## Optimisations Used + +### 1. Stop Materialising Per-Set `q_com` + +PCS block 3 now computes only the scalar folds `q_eval_set[s]`. + +`q_eval_set` is still required by block 4, which evaluates the interpolated +polynomial `f` at `x3`. The commitment counterpart `q_com[s]` is not required +as a standalone point once the final commitment is computed directly. + +### 2. Fuse `q_com` and `final_com` Into One G1MSM + +The final commitment uses linearity to combine the two old stages: + +```text +final_com + = sum_s x4^s * q_com[s] + x4^n_sets * f_com + = sum_{s,i} (x4^s * x1^i) * C[s][i] + x4^n_sets * f_com +``` + +The generated verifier now stages all non-identity commitment terms into one +G1MSM input buffer and calls the EIP-2537 G1MSM precompile once. + +The staged scalar for each commitment is: + +```text +set 0: x1^i +set s > 0: x4^s * x1^i +f_com: x4^n_sets +``` + +When truncated challenges are enabled, the generated `x4` powers preserve the +same semantics as Midnight proofs: the internal power accumulator is full width, +and each emitted power is truncated to 128 bits. + +### 3. Skip G1 Identity Commitments in the MSM + +Committed-instance commitments are represented by the G1 identity point. They +must still contribute their scalar evaluations to `q_eval_set`, but they do not +change `final_com`. + +The final fused MSM filters out identity commitments while preserving their +evaluation contribution in block 3. This avoids paying a pair slot for a point +that contributes zero to the commitment fold. + +### 4. Reuse PCS Scratch Memory + +The codegen path now builds the point-set grouping once and reuses the same PCS +scratch region for: + +- q-eval address tables, +- the final fused MSM input buffer, +- final commitment output staging. + +This reduces duplicated staging logic and keeps the generated verifier smaller. + +### 5. Keep `v` and `f_eval` Semantics Unchanged + +The optimisation changes only the commitment-side fold. The scalar side remains: + +```text +v = sum_s x4^s * q_eval[s] + x4^n_sets * f_eval +``` + +Block 4 still computes `f_eval` from the `q_eval_set` vectors with Lagrange +interpolation at `x3`, so the pairing equation is unchanged. + +### 6. Associated Cleanup + +The same commit also: + +- renames gas labels from `q_com/q_eval fold` to `q_eval fold`, +- updates the IVC replay harness to deploy and wire `Halo2QuotientEvaluator`, +- scopes quotient helper arithmetic through a local `q_r := FR_MODULUS` alias to + trim emitted helper code. + +## Measured IVC Bench Result + +Command: + +```sh +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ + scripts/run_ivc_bench.sh +``` + +Result: + +```text +PASS: final Keccak IVC proof accepted on-chain +total tx gas: 1,843,843 +real section work: 1,681,292 +checkpoint overhead: 18,750 +``` + +Contract sizes: + +| contract | runtime bytes | +| --- | ---: | +| Halo2Verifier | 12,614 | +| Halo2VerifyingKey | 13,568 | +| Halo2QuotientEvaluator | 21,755 | +| total deployed runtime | 47,937 | + +Relevant PCS sections: + +| id | gas | section | +| ---: | ---: | --- | +| 19 | 6,584 | PCS block 3 set 0 (q_eval fold) | +| 20 | 241 | PCS block 3 set 1 (q_eval fold) | +| 21 | 185 | PCS block 3 set 2 (q_eval fold) | +| 22 | 2,863 | PCS block 3 set 3 (q_eval fold) | +| 23 | 1,219 | PCS block 3 set 4 (q_eval fold) | +| 24 | 21,880 | PCS block 4 (f_eval Lagrange interpolation) | +| 25 | 458,782 | PCS block 5 (final_com x4-power MSM + v) | + +The main benefit is that block 3 no longer pays for per-set commitment MSMs. +The remaining PCS cost is concentrated in the single fused final commitment MSM, +which is now the unavoidable large commitment fold rather than duplicate +intermediate work. + +## Validation Commands + +```sh +cargo check --features evm,truncated-challenges,in-circuit-fewer-point-sets,solidity-gas-checkpoints + +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ + cargo test --release \ + --features evm,truncated-challenges,solidity-gas-checkpoints \ + --test poseidon_fixture poseidon_renders_compiles_and_verifies \ + -- --ignored --nocapture + +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ + cargo test --release \ + --features evm,truncated-challenges,in-circuit-fewer-point-sets,solidity-gas-checkpoints \ + --test ivc_keccak_solidity ivc_final_keccak_solidity_e2e \ + -- --ignored --nocapture +``` diff --git a/proofs/solidity-verifier/docs/plans/FURTHER_OPTIMISATIONS.md b/proofs/solidity-verifier/docs/plans/FURTHER_OPTIMISATIONS.md new file mode 100644 index 000000000..de48fa980 --- /dev/null +++ b/proofs/solidity-verifier/docs/plans/FURTHER_OPTIMISATIONS.md @@ -0,0 +1,186 @@ +# Further Gas / Bytecode Optimisation Candidates + +This note captures the next optimisation ideas after the current generator +state: compact quotient VM, split quotient evaluator, fused PCS MSM, MCOPY +staging, batch inversion, public-accumulator fast paths, and final pairing +batching are already in place. + +The remaining wins are mostly profile-specific. Pin the exact benchmark shape +before measuring deltas: the gas-oriented quotient profile and the smaller +compile-stable quotient profile make different tradeoffs, and +`scripts/run_ivc_bench.sh` may also change the result depending on whether +`outer-single-h-commitment` is enabled. + +## 1. Gate production quotient trace hooks + +Status: applied. + +`templates/partials/quotient_numerator/QuotientNumeratorBlock.yul` maintains a trace id and calls +`trace_u256` on every quotient fold even in production. In the external +quotient evaluator the logless hook still writes to memory. + +Potential win: + +- Lower gas in the batched identity numerator checkpoint. +- Some bytecode reduction in the quotient evaluator. + +Risk / caveat: + +- Existing comments say the hook is kept to stabilize the via-IR arithmetic + shape. Benchmark both gas and runtime size after removing or gating it. + +Validation run after applying the gate: + +```text +command: scripts/run_ivc_bench.sh --skip-srs-download +shape: current worktree script default, outer single-H disabled +result: PASS +total tx gas: 1,300,701 +real section work: 1,143,579 +batched numerator section: 313,762 +verifier runtime: 12,061 bytes +VK runtime: 15,136 bytes +quotient evaluator runtime: 23,448 bytes +``` + +## 2. Remove the selector y^-1 fold scheme + +Status: applied, but measured as a bytecode win and a small gas regression for +the current IVC Keccak bench shape. + +When simple selectors exist, the quotient VM computes `y_inv` with modexp and +updates `sel_scale` / `sel_inv_scale` for every identity. The selector identity +positions are known at codegen time. + +Alternative: + +- Emit gap-based selector bucket updates. +- Scale selector buckets only when a selector identity appears. +- Apply the final y-power gap once at the end. + +Potential win: + +- Remove one modexp in the quotient evaluator. +- Reduce repeated per-identity memory loads/stores and `mulmod`s. + +Risk / caveat: + +- The current forward-scan logic carefully matches Rust's reverse y-power + selector grouping. The replacement needs trace-equivalence tests. + +Validation run after applying the gap fold: + +```text +command: scripts/run_ivc_bench.sh --skip-srs-download +shape: current worktree script default, outer single-H disabled +result: PASS +total tx gas: 1,301,676 +real section work: 1,144,410 +batched numerator section: 314,581 +verifier runtime: 12,061 bytes +VK runtime: 15,168 bytes +quotient evaluator runtime: 22,698 bytes +total runtime: 49,927 bytes +``` + +Compared with the trace-gated baseline above, this saves 750 bytes in the +quotient evaluator runtime and 718 bytes overall, but increases checkpoint 12 +by 819 gas and total transaction gas by 975. Keep this change only when runtime +size is the priority, or use it as a base for a cheaper selector-power scheme. + +## 3. Replace native-gate top-N selection with a byte-budget knapsack + +`native_gate_indices` currently picks the heaviest remaining gate identities by +compact-VM byte length. That does not necessarily maximize gas saved per byte +of extra native Yul. + +Alternative: + +- Estimate native gas saved per identity. +- Estimate native bytecode growth per identity. +- Select identities under the quotient evaluator runtime budget. +- Allow skipping a problematic identity, such as the currently unsafe fifth + native gate, while trying the next best candidate. + +Potential win: + +- More quotient gas reduction without crossing EIP-170. +- A safer path than simply raising `CodegenConfig::quotient_native_gates`. + +Risk / caveat: + +- Requires full proof validation for every selected identity set. + +## 4. Make pinned dependency addresses non-public + +The verifier exposes `AUTHORIZED_VK` and `AUTHORIZED_QUOTIENT` as public +immutables, which generates runtime getter code. + +Alternative: + +- Use `internal` or `private` immutables if external callers do not need the + getters. + +Potential win: + +- Small verifier runtime bytecode reduction. + +Risk / caveat: + +- Loses a convenient on-chain introspection API unless a separate explicit + getter is retained. + +## 5. Specialize PCS q_eval source tables + +The rolled q_eval path stages eval source addresses into scratch every proof. +Many generated layouts are contiguous or fixed-stride in memory. + +Alternative: + +- Detect contiguous/fixed-stride eval layouts at codegen time. +- Derive source addresses directly inside the q_eval loop. +- Fall back to the current staged address table for irregular layouts. + +Potential win: + +- Small gas and bytecode reduction in PCS block 3 / q_eval folds. + +Risk / caveat: + +- Likely modest compared with quotient and final MSM costs. + +## 6. Optional: defer G1 coordinate bounds to EIP-2537 + +`common_uncompressed_g1` checks Fp coordinate bounds before hashing absorbed +points. If every absorbed proof point is guaranteed by the generated protocol +plan to be consumed by an EIP-2537 G1MSM or pairing call, the precompile will +eventually reject invalid coordinates. + +Alternative: + +- Keep the high-pad-byte canonicality checks before transcript absorption. +- Defer full coordinate validity to the later EIP-2537 precompile path. + +Potential win: + +- Small accepted-proof gas reduction in transcript commitment reads. + +Risk / caveat: + +- Invalid proofs fail later and spend more gas. +- This is a policy/security-review choice, not a free micro-optimisation. + +## Structural wins outside local codegen + +The fused PCS final MSM is now mostly bounded by EIP-2537 pair count. Real +movement there comes from proof and circuit shape rather than local verifier +emission: + +- Enable/keep outer single-H when acceptable. +- Reduce opened advice/fixed/permutation/lookup/trash commitments. +- Reduce lookup helper commitments. +- Reduce queried rotations or columns. +- Reduce quotient commitment terms. + +Each removed final-MSM term is expected to save roughly `6k-8k` gas, depending +on the surrounding G1MSM size and calldata changes. diff --git a/proofs/solidity-verifier/docs/plans/IMPROVEMENTS.md b/proofs/solidity-verifier/docs/plans/IMPROVEMENTS.md new file mode 100644 index 000000000..6ac1908f0 --- /dev/null +++ b/proofs/solidity-verifier/docs/plans/IMPROVEMENTS.md @@ -0,0 +1,71 @@ +# Improvements + +Yes. We can borrow the architecture, not assume the audit transfers to us. +Axiom's README says `snark-verifier` v0.1.1+ completed Trail of Bits audits, +but our generated BLS12-381/Midnight assembly would still need its own review +surface. + +The best ideas to take: + +1. **Typed verifier pipeline** + `snark-verifier` separates proof-system semantics from backend lowering with + generic `Loader`, `ScalarLoader`, and `EcPointLoader` traits. Same verifier + logic can target native, Halo2, or EVM backends. This is much safer than + hand-emitting Yul from proof-system code. + +2. **Protocol AST before assembly** + It compiles Halo2 into a `PlonkProtocol` with an expression AST like + `Expression::{Constant, Polynomial, Challenge, Sum, Product, Scaled, + DistributePowers}`. The quotient is represented as structured data, not + strings. + +3. **Centralized lowering** + The EVM backend has a small scalar AST, + `Value::{Constant, Memory, Negated, Sum, Product}`, then one lowering path + decides how to emit Yul/compact IR. That gives one place to audit arithmetic + emission. + +4. **Expression cache / CSE** + Their `EvmLoader::scalar` caches expression identifiers to memory slots. We + already adopted the quotient version with global VM CSE. + +5. **Explicit sum/sum-product helpers** + Their `ScalarLoader` has `sum_with_coeff_and_const` and + `sum_products_with_coeff_and_const`. We just added the quotient-side version, + which saved ~7.4k gas. + +6. **Typed compact IR** + Their compact backend lowers into enum instructions like `ScalarAdd`, + `ScalarMul`, `ScalarMulAdd...`, calldata loads, point loads, staticcalls, + etc. That is much safer than free-form Yul snippets. + +## What I Would Apply Here + +The biggest safety win is replacing our "emit Yul strings, then parse them back +into quotient expressions" path with a typed quotient IR: + +```text +Midnight/Halo2 constraints + -> QuotientExpr AST + -> normalization passes + -> CSE / sum-product folding + -> checked VM program or checked inline Yul + -> generated Solidity +``` + +Then add a verifier for the lowering: + +- stack never underflows +- all temp slots initialized before read +- constant/program indices in range +- memory regions do not overlap +- every opcode has fixed semantics +- generated VM/native evaluation match on random environments +- generated Solidity checkpoint values match native verifier values + +This would not automatically reduce gas, but it would make the assembly much +more auditable and soundness-oriented. For gas, we still need hybrid inlining or +helper/sharded straight-line evaluators. + +Sources: Axiom repo README and audit note, `snark-verifier` docs/source, and +local checkout paths under `/Users/Julien.Coolen/snark-verifier`. diff --git a/proofs/solidity-verifier/docs/plans/IVC_GAS_OPTIMISATION_ACTION_PLAN.md b/proofs/solidity-verifier/docs/plans/IVC_GAS_OPTIMISATION_ACTION_PLAN.md new file mode 100644 index 000000000..da1fce13f --- /dev/null +++ b/proofs/solidity-verifier/docs/plans/IVC_GAS_OPTIMISATION_ACTION_PLAN.md @@ -0,0 +1,431 @@ +# IVC Gas Optimisation Action Plan + +Baseline command: + +```sh +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ + scripts/run_ivc_bench.sh +``` + +Baseline result: + +```text +PASS +total tx gas: 1,843,843 +real checkpointed work: 1,681,292 +checkpoint overhead: 18,750 +verifier runtime: 12,614 bytes +VK runtime: 13,568 bytes +quotient evaluator runtime: 21,755 bytes +total deployed runtime: 47,937 bytes +``` + +## Current Cost Centers + +Original baseline: + +| id | gas | section | +| ---: | ---: | --- | +| 12 | 631,306 | batched identity numerator reconstruction | +| 25 | 458,782 | PCS block 5, final_com x4-power MSM + v | +| 15 | 140,561 | public accumulator pairing check | +| 13 | 119,613 | linearization-commitment MSM | +| 10 | 107,867 | evaluations + x1/x2 + f_com + x3 + q_evals + x4 + pi | +| 16 | 103,168 | final proof ec_pairing | + +After applied optimisations 1, 2, and 4: + +| id | gas | section | +| ---: | ---: | --- | +| 12 | 631,306 | batched identity numerator reconstruction | +| 25 | 537,088 | PCS block 5, final_com x4-power MSM + v | +| 10 | 107,867 | evaluations + x1/x2 + f_com + x3 + q_evals + x4 + pi | +| 16 | 103,168 | final proof ec_pairing | +| 15 | 38,875 | public accumulator pairing batch prep | +| 14 | 25,435 | PCS block 6, pairing inputs LHS/RHS | + +## Optimisation Queue + +### 1. Public Accumulator Scalar Fast Path + +Status: applied and benchmarked. + +The public accumulator block rebuilds accumulator points from public inputs and +then uses G1MSM even for scalar `1`. For collapsed IVC accumulators, the public +shape is commonly: + +```text +lhs point, lhs scalar = 1 +rhs point, rhs scalar = 1 +``` + +Fast paths: + +```text +scalar == 0 -> write G1 identity +scalar == 1 -> keep the loaded point +otherwise -> call G1MSM +``` + +Expected win: `25k-45k` gas when accumulator scalars are `1`. + +Risk: low. This only skips mathematically redundant scalar multiplication. + +Implementation: + +- LHS accumulator scalar: + - `0` writes the identity point, + - `1` keeps the decoded point in place, + - any other scalar uses the existing one-pair G1MSM path. +- RHS accumulator scalar: + - `0` skips the variable-base pair, + - `1` keeps the decoded point directly when there is no fixed-base tail, + - otherwise uses the existing MSM staging path. + +Validation command: + +```sh +cargo check --features evm,truncated-challenges,in-circuit-fewer-point-sets,solidity-gas-checkpoints + +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ + scripts/run_ivc_bench.sh +``` + +Benchmark result: + +```text +PASS +total tx gas: 1,819,533 +real checkpointed work: 1,657,102 +verifier runtime: 12,704 bytes +VK runtime: 13,568 bytes +quotient evaluator runtime: 21,755 bytes +total deployed runtime: 48,027 bytes +``` + +Delta versus baseline: + +```text +total tx gas: 1,843,843 -> 1,819,533 (-24,310) +real checkpointed work: 1,681,292 -> 1,657,102 (-24,190) +verifier runtime: 12,614 -> 12,704 (+90 bytes) +``` + +Section delta: + +```text +public accumulator pairing check: 140,561 -> 116,371 (-24,190) +all other checkpoint deltas: unchanged +``` + +### 2. Fuse Linearization MSM Into PCS Final MSM + +Status: applied and benchmarked. + +Checkpoint 13 materializes: + +```text +LINEARIZATION_COM = + (1 - x^n) * sum_i x_split^i * Q_i + + sum_j selector_acc_j * S_j +``` + +PCS block 5 later uses this linearized commitment as one term of the final +fused MSM. By linearity, the block 5 scalar for the linearized commitment can be +pushed into each quotient-limb and simple-selector term: + +```text +lambda * LINEARIZATION_COM +``` + +becomes: + +```text +lambda * (1 - x^n) * x_split^i * Q_i +lambda * selector_acc_j * S_j +``` + +Expected win: `30k-55k` gas if the extra pairs are cheaper than a separate MSM +precompile call. + +Risk: medium. Requires keeping PCS query ordering, `q_eval_set`, and identity +commitment handling unchanged. + +Implementation: + +- Checkpoint 13 now only computes and stores `x_split` and `1 - x^n`. +- PCS block 5 recognizes the linearization commitment query and expands it into: + - quotient-limb pairs `Q_i` with scalar + `lambda * (1 - x^n) * x_split^i`, + - simple-selector fixed commitment pairs with scalar + `lambda * selector_acc_j`. +- PCS scratch was moved past the selector accumulator words. The first attempt + reused the old scratch start, which overwrote selector accumulators before + PCS block 5 consumed them. + +Validation command: + +```sh +cargo check --features evm,truncated-challenges,in-circuit-fewer-point-sets,solidity-gas-checkpoints + +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ + scripts/run_ivc_bench.sh +``` + +Benchmark result after optimisations 1 and 2: + +```text +PASS +total tx gas: 1,781,145 +real checkpointed work: 1,618,642 +verifier runtime: 12,859 bytes +VK runtime: 13,568 bytes +quotient evaluator runtime: 21,755 bytes +total deployed runtime: 48,182 bytes +``` + +Delta versus original baseline: + +```text +total tx gas: 1,843,843 -> 1,781,145 (-62,698) +real checkpointed work: 1,681,292 -> 1,618,642 (-62,650) +verifier runtime: 12,614 -> 12,859 (+245 bytes) +``` + +Delta versus optimisation 1 only: + +```text +total tx gas: 1,819,533 -> 1,781,145 (-38,388) +real checkpointed work: 1,657,102 -> 1,618,642 (-38,460) +verifier runtime: 12,704 -> 12,859 (+155 bytes) +``` + +Section deltas versus optimisation 1 only: + +```text +linearization commitment/scalar prep: 119,613 -> 2,826 (-116,787) +PCS final fused MSM: 458,782 -> 537,088 (+78,306) +net section work: -38,481 +``` + +Latest detailed section bench: + +```text +id gas section +2 4,528 VK loading +3 12,247 VK digest + committed_pi + instance absorbs +4 6,207 user-phase advice reads + user challenge squeezes +5 3,315 theta squeeze + lookup multiplicities +6 5,275 beta/gamma + permutation Z products +7 1,116 lookup helpers + Z accumulators +8 2,589 trash_challenge + trashcans +9 2,976 y squeeze + quotient-limb reads +10 107,867 evaluations + x1/x2 + f_com + x3 + q_evals + x4 + pi +11 18,190 Lagrange + instance evaluation +12 631,306 batched identity numerator reconstruction +13 2,826 linearization scalar prep +17 215 PCS block 1, rotation points +18 5,624 PCS block 2, x1 powers +19 6,661 PCS block 3 set 0, q_eval fold +20 241 PCS block 3 set 1, q_eval fold +21 185 PCS block 3 set 2, q_eval fold +22 2,863 PCS block 3 set 3, q_eval fold +23 1,219 PCS block 3 set 4, q_eval fold +24 21,880 PCS block 4, f_eval Lagrange interpolation +25 537,088 PCS block 5, final_com x4-power MSM + v +14 25,435 PCS block 6, pairing inputs LHS/RHS +15 116,371 public accumulator pairing check +16 103,168 final proof ec_pairing +``` + +### 3. Quotient Native Callbacks / VM Opcodes + +Status: measured; no additional safe change applied. + +Checkpoint 12 is the largest section. The current quotient evaluator fits under +24 KiB but pays VM dispatch overhead. Available knobs: + +```text +CodegenConfig::quotient_native_gates +CodegenConfig::quotient_native_permutation +CodegenConfig::quotient_vm_cse +CodegenConfig::quotient_encoding +``` + +Known constraint: raising native gates too far previously broke the IVC verifier +or exhausted helper bytecode headroom. The safe next step is an A/B sweep of +small values with full bench validation and contract-size tracking. + +Expected win: `50k-200k` gas if more heavy identities can be native without +breaking the quotient evaluator size budget. + +Risk: medium. + +Measured sweeps: + +```text +default: + native permutation: on + native gates: 4 + VM CSE: on + encoding: bytes + quotient runtime: 21,755 bytes + result: PASS + +CodegenConfig { quotient_native_gates: 5, ..Default::default() }: + quotient runtime: 24,464 bytes + result: FAIL, verifier reverted at gas_used = 1,755,588 + note: fits just under 24 KiB but is not currently correct for this proof. + +CodegenConfig { quotient_native_gates: 6, ..Default::default() }: + quotient runtime: 30,532 bytes + result: FAIL, over EIP-170 helper budget and verifier reverted + +CodegenConfig { quotient_encoding: Packed32, quotient_limb_vm_ops: false, ..Default::default() }: + quotient runtime: 21,353 bytes + VK runtime: 16,448 bytes + result: FAIL, verifier reverted after consuming almost the whole gas limit +``` + +Conclusion: + +The current quotient settings are already at the safe point for this bench. +More native gate callbacks need a correctness fix before they can be used, and +`packed32` is not safe in the current generated verifier. The next quotient +work should be a targeted debugging pass for native gate 5, not a blind gas +optimisation. + +### 4. Batch Public Accumulator Pairing With Final KZG Pairing + +Status: applied and benchmarked. + +The verifier currently performs two separate pairings: + +```text +public accumulator pairing +final KZG pairing +``` + +The two equations use the same G2 bases, so we can combine in G1 and keep a +single 2-pair pairing check: + +```text +combined_rhs = KZG_RHS + alpha * ACC_RHS +combined_lhs = KZG_LHS + alpha * ACC_LHS +check e(combined_rhs, G2) * e(combined_lhs, -sG2) == 1 +``` + +`alpha` is derived after all four G1 pairing inputs are fixed: + +```text +alpha = keccak256(domain || KZG_RHS || KZG_LHS || ACC_RHS || ACC_LHS) mod Fr +``` + +This avoids the unsound deterministic product check where two invalid pairing +equations could cancel. + +Benchmark result after optimisations 1, 2, and 4: + +```text +PASS +total tx gas: 1,703,601 +real checkpointed work: 1,541,146 +verifier runtime: 13,008 bytes +VK runtime: 13,568 bytes +quotient evaluator runtime: 21,755 bytes +total deployed runtime: 48,331 bytes +``` + +Delta versus optimisation 1+2: + +```text +total tx gas: 1,781,145 -> 1,703,601 (-77,544) +real checkpointed work: 1,618,642 -> 1,541,146 (-77,496) +verifier runtime: 12,859 -> 13,008 (+149 bytes) +``` + +Delta versus original baseline: + +```text +total tx gas: 1,843,843 -> 1,703,601 (-140,242) +real checkpointed work: 1,681,292 -> 1,541,146 (-140,146) +verifier runtime: 12,614 -> 13,008 (+394 bytes) +``` + +Section delta: + +```text +public accumulator pairing check/prep: 116,371 -> 38,875 (-77,496) +final proof ec_pairing: 103,168 -> 103,168 (unchanged) +``` + +Latest detailed section bench: + +```text +id gas section +2 4,528 VK loading +3 12,247 VK digest + committed_pi + instance absorbs +4 6,207 user-phase advice reads + user challenge squeezes +5 3,315 theta squeeze + lookup multiplicities +6 5,275 beta/gamma + permutation Z products +7 1,116 lookup helpers + Z accumulators +8 2,589 trash_challenge + trashcans +9 2,976 y squeeze + quotient-limb reads +10 107,867 evaluations + x1/x2 + f_com + x3 + q_evals + x4 + pi +11 18,190 Lagrange + instance evaluation +12 631,306 batched identity numerator reconstruction +13 2,826 linearization scalar prep +17 215 PCS block 1, rotation points +18 5,624 PCS block 2, x1 powers +19 6,661 PCS block 3 set 0, q_eval fold +20 241 PCS block 3 set 1, q_eval fold +21 185 PCS block 3 set 2, q_eval fold +22 2,863 PCS block 3 set 3, q_eval fold +23 1,219 PCS block 3 set 4, q_eval fold +24 21,880 PCS block 4, f_eval Lagrange interpolation +25 537,088 PCS block 5, final_com x4-power MSM + v +14 25,435 PCS block 6, pairing inputs LHS/RHS +15 38,875 public accumulator pairing batch prep +16 103,168 final proof ec_pairing +``` + +### 5. Proof-Shape Reduction For The 65-Pair Final MSM + +Status: assessed; not implemented in this patch. + +The fused PCS final MSM is a 65-pair G1MSM. Local point-set changes do not +reduce this pair count; `fewer-point-sets` was measured to keep the same +65-pair MSM and worsen gas through dummy eval overhead. + +Useful changes are proof-shape changes: + +- fewer opened advice commitments, +- fewer fixed/permutation/lookup/trash commitments, +- fewer lookup helper commitments, +- fewer quotient limbs, +- fewer queried rotations/columns. + +Expected win: roughly `6k-8k` gas per removed commitment in the final MSM. + +Risk: circuit/protocol dependent. + +Conclusion: + +No local verifier patch can remove these final MSM terms after the current +fusion. The earlier `fewer-point-sets` experiment confirmed that point-set +layout changes did not reduce the 65-pair fused MSM; they added dummy eval work +instead. Real savings here require changing the circuit/proof shape. + +## Reporting Template + +For each implemented optimisation, record: + +```text +command: +result: +total tx gas: +real checkpointed work: +runtime sizes: +section deltas: +notes: +``` diff --git a/proofs/solidity-verifier/docs/plans/ROADMAP.md b/proofs/solidity-verifier/docs/plans/ROADMAP.md new file mode 100644 index 000000000..1db1fbc89 --- /dev/null +++ b/proofs/solidity-verifier/docs/plans/ROADMAP.md @@ -0,0 +1,555 @@ +# Production Readiness Roadmap + +This repository is not production-ready yet. + +The current tests are useful and have caught real regressions, but this is +cryptographic verifier code. The production bar is higher than happy-path proof +acceptance plus random proof mutation. The verifier should not be used for +meaningful value while known High and Medium issues remain open in +[`AUDIT.md`](./AUDIT.md). + +## Current Strengths + +- End-to-end IVC proof acceptance through `scripts/run_ivc_bench.sh`. +- Section-level gas and size instrumentation. +- Property tests for wrong instances, proof mutation, wrong VKs, and malformed + calldata. +- Deterministic rendering and compilation checks. +- Rust/Solidity trace-comparison tooling. +- Bench artifacts dumped for replay under `target/ivc-keccak-solidity-dump/`. + +These are a strong base, but they are not enough for production cryptographic +assurance. + +## Production Blockers + +Fix known verifier-soundness and deployment issues before any production use: + +1. Pin the quotient evaluator by generated codehash and runtime length. +2. Check `returndatasize()` after every precompile call. +3. Add precompile self-tests or deployment guards for EIP-2537 behavior. +4. Fix the Rust `read_g1` transcript asymmetry. +5. Reject malformed and non-canonical G1 inputs consistently. +6. Reject zero inversion denominators. +7. Add an end-of-proof cursor check after parsing `pi`. + +Every fix above needs a regression test that fails on the old behavior. + +## Required Test Expansion + +### Differential Testing + +For every generated verifier variant, compare Solidity behavior against the +Rust verifier: + +- transcript challenges, +- quotient numerator values, +- selector accumulators, +- PCS `q_eval` folds, +- `f_eval`, +- final MSM / `final_com`, +- pairing inputs, +- final accept/reject result. + +Cover at least: + +- compact quotient VM, +- native quotient callbacks, +- external quotient contract, +- fused PCS final MSM, +- accumulator pairing batching, +- `fewer-point-sets` on and off, +- truncated challenges, +- simple selectors, +- lookups, +- permutations, +- trash gates. + +The quotient evaluator especially needs straight-line vs VM vs external-contract +differential tests. + +### Adversarial Negative Tests + +Add targeted malformed-proof tests, not only random mutation: + +- malformed G1 padding, +- off-curve and subgroup-invalid points, +- unused or zero-weight commitments, +- wrong quotient evaluator contract, +- wrong or absent precompile behavior in a test harness, +- boundary field values: `0`, `1`, `r - 1`, `r`, and non-canonical encodings, +- denominator-zero cases where toy circuits can force them. + +### Codegen Invariant Tests + +Add tests that prove generated layouts and bindings are internally consistent: + +- memory regions do not overlap, +- proof cursor consumes exactly the expected bytes, +- `proof_len`, `num_evals`, `num_point_sets`, and dummy eval counts match the + native proof parser, +- generated verifier, VK, and quotient evaluator are mutually bound, +- trace and gas-checkpoint builds cannot be accidentally used as production + artifacts. + +## CI Gate + +Before production, CI should run at least: + +```bash +cargo test --workspace --all-features --all-targets + +cargo test --release --workspace --all-features --all-targets \ + -- --include-ignored + +scripts/run_ivc_bench.sh --check-only + +scripts/run_ivc_bench.sh +``` + +For release candidates, CI should also run the native Midfall comparison: + +```bash +scripts/run_ivc_bench.sh --native-midfall +``` + +## Deployment Hardening + +Production deployment must be reproducible and pinned: + +- fixed Rust toolchain, +- fixed `solc` version, +- locked dependencies, +- explicit feature set, +- reproducible verifier/VK/quotient runtime bytecode, +- published runtime bytecode hashes, +- automated address wiring, +- chain compatibility check for EIP-2537 addresses and semantics, +- no manual deployment path that can bypass quotient/VK codehash checks. + +Deployment artifacts should include: + +- verifier runtime hash, +- VK runtime hash, +- quotient evaluator runtime hash, +- compiler version, +- feature flags, +- source commit, +- expected contract sizes, +- expected bench result. + +## External Review + +After fixing the known issues and expanding tests: + +1. Run an internal security review focused on soundness, transcript equivalence, + calldata canonicality, memory layout, and deployment binding. +2. Freeze a release candidate. +3. Commission an independent external audit. +4. Fix all High and Medium findings. +5. Run a public testnet period with pinned bytecode and monitored proofs. + +## Minimum Production Bar + +Treat the verifier as production-ready only when all of the following are true: + +- no known High or Medium verifier-soundness findings remain open, +- every fixed audit issue has a regression test, +- Rust/Solidity differential tests cover the active feature matrix, +- adversarial malformed-proof tests pass, +- deployment is reproducible and hash-pinned, +- target-chain precompile compatibility is verified, +- an independent external audit has been completed, +- the exact production bytecode has survived a testnet soak period. + +Until then, this code should be considered experimental cryptographic +infrastructure. + +## Halo2/KZG Solidity Audit Checklist + +This checklist is for implementing or auditing a Halo2/KZG verifier in Solidity +using BLS12-381 precompiles. The source reports behind it are mostly BN254 +PLONK/Groth16/Halo2 verifier audits, but the same bug classes apply here. Some +are sharper for BLS because coordinates are 381-bit, subgroup/cofactor handling +matters more, and Solidity/Yul must marshal non-`uint256` field elements +carefully. + +### 1. Pin The Exact Protocol And Trust Model + +Define the verifier as a precise protocol, not just "Halo2 verifier": + +- Exact Halo2 variant: KZG vs IPA; SHPLONK/multi-opening scheme; + aggregation/accumulator scheme; single proof vs batch proof. +- Exact transcript: challenge order, hash function, domain separators, + encodings, challenge-to-field method. +- Exact proving key/verifying key relationship: domain size `k`, number of + instance columns, rotations, commitments, fixed/advice/lookup configuration. +- Exact SRS/CRS assumption: whether a trusted setup is required, which + ceremony, which powers are included, and whether the verifier is bound to + that SRS. + +This is not optional. The Linea audit framed verifier correctness as +consistency with the PLONK paper and prover implementation, proof +soundness/completeness, and absence of unintended edge cases. It also found +"No Proper Trusted Setup" as critical because an unsafe CRS can allow forged +proofs. + +### 2. Bind The Verifier To The Right VK, SRS, Circuit, And Chain Context + +Things to bind or hardcode: + +- `vk_digest` / circuit digest / verifying-key hash. +- SRS elements used by the verifier, especially `[x]_2` or equivalent BLS G2 + SRS element. +- Domain parameters: `n`, `omega`, `omega_inv`, `n_inv`, rotations, cosets. +- Number and order of public inputs and instance columns. +- Protocol version, curve ID, transcript version, and proof layout version. +- Chain ID and verifier contract address when the proof authorizes an on-chain + state transition, withdrawal, bridge action, or nullifier update. + +The Espresso audit found that the transcript omitted common preprocessed input +and SRS elements, introducing an unexpected degree of freedom; it recommended +including `[x]_2` in the verifying key. Scroll's Halo2 audit similarly flagged +Fiat-Shamir transcripts that did not include elliptic-curve parameters and +recommended including all public parameters in the transcript. + +### 3. Treat Calldata And Proof Layout As Consensus-Critical + +For a generated Solidity verifier, calldata parsing is part of the proof +system. Check: + +- Exact proof length; reject missing and extra bytes. +- Exact public input count. +- No dynamic array overlap or assumptions about ABI layout. +- No unused appended bytes ignored by the verifier. +- Fixed offsets for every commitment/evaluation. +- Separate parsers for compressed vs uncompressed points. +- Explicit endian convention. +- For BLS: handle 48-byte Fq elements and 96-byte Fq2 elements carefully; do + not truncate them into `uint256`. + +The Linea audit found a missing proof length check that could cause proof and +public-input memory overlap or allow ignored extra data. Espresso also found +public input handling problems: dynamic arrays were accepted while only the +first eight public inputs were validated and used. + +### 4. Enforce Canonical Field Encodings Everywhere + +Do not reduce prover-supplied values unless the protocol explicitly specifies +reduction before hashing and verification. Prefer rejection. + +Check all: + +- Public inputs `< Fr`. +- Scalar evaluations `< Fr`. +- MSM scalars `< Fr`. +- Challenge outputs `< Fr`. +- Fq coordinates `< q`. +- Fq2 coordinates: both limbs `< q`. +- Compressed point encodings are canonical and unique. +- No `x + q`, `s + r`, or multiple encodings of the same element accepted. + +Linea found missing range checks for public inputs and scalar proof elements. +Risc0's Groth16 audit warned that reducing public inputs modulo the group order +creates multiple representations; it recommended asserting that inputs are +already reduced. Espresso found non-canonical G1 deserialization and broader +ambiguity about canonical scalar/field arguments. + +### 5. Validate All Curve Points Explicitly, Not Only Via Precompiles + +For BLS12-381, this is especially important: + +- Check G1/G2 points are canonically encoded. +- Check on-curve. +- Check subgroup membership. +- Decide whether point at infinity is allowed. Usually reject proof + commitments/opening proofs unless the protocol explicitly permits infinity. +- Ensure negation is only applied to reduced, on-curve points. +- Ensure compressed point sign bits are canonical and match the off-chain + implementation. +- Ensure G2 Fq2 coefficient ordering matches the precompile spec and prover + library. + +Linea recommended explicit field/curve checks for proof elements even when +precompiles also check, because relying on precompile failure gives poor errors +and can create unintended behavior. Axiom's Halo2 audit found multiple edge +case failures around elliptic-curve operations, including point-at-infinity +handling and missing on-curve checks for loaded witness points. + +### 6. Wrap BLS Precompiles With Hardened Tiny Wrappers + +Every precompile wrapper should enforce: + +- Exact input length. +- Exact output length. +- Fresh output memory or cleared memory before call. +- `staticcall` success checked. +- `returndatasize()` checked. +- Pairing result value checked, not just call success. +- No arbitrary `sub(gas(), 2000)` unless justified; pass available gas or a + carefully bounded amount. +- No stale memory reuse if the call fails. +- Precompile behavior documented per target chain/L2. + +Linea's PLONK audit had two critical precompile-related issues: it failed to +check the pairing result stored in memory, so invalid proofs could pass, and +several `staticcall`s ignored failure, allowing stale memory to be used for +challenges, exponentiation, point addition, or scalar multiplication. Espresso +also flagged inconsistent staticcall offsets and unnecessary gas subtraction in +curve wrapper code. + +### 7. Get Finite-Field Arithmetic Edge Cases Right + +Critical arithmetic checks: + +- Never define `inv(0) = 0`; revert or branch explicitly. +- Batch inversion must precheck all denominators nonzero, or correctly handle + zero denominators in a protocol-specific way. +- Lagrange evaluation must handle `zeta` being a root of unity. +- Use `addmod`/`mulmod` for Fr arithmetic; avoid plain `add` unless there is a + proof that overflow and modulus mismatch cannot occur. +- For BLS Fq arithmetic, do not use `uint256` arithmetic; use precompiles or + multi-limb logic. +- Check domain size and root-of-unity constants. +- Check `n_inv * n = 1 mod Fr`, `omega^n = 1`, correct two-adicity, coset + uniqueness, and rotation constants. + +Linea and Espresso both found the same high-impact Lagrange-at-root-of-unity +issue: the efficient formula returns zero when `zeta` is in the domain, but the +correct Lagrange value may be one. Both also flagged zero inversion as invalid. + +### 8. Reproduce The Transcript Exactly + +For Halo2, transcript mismatches are a common source of silent unsoundness or +incompatibility. + +Consider: + +- Absorb VK/SRS/circuit digest before proof messages. +- Absorb all public inputs, including lengths and indices. +- Absorb every prover message before deriving the next challenge. +- Include curve ID and encoding mode. +- Domain-separate challenge rounds. +- Use unambiguous fixed-width encodings. +- Avoid `abi.encodePacked` ambiguity for variable-length data. +- Do not prepend/rehash in a way that deviates from the reference transcript + unless formally justified. +- Ensure hash precompile calls cannot fail silently. + +Espresso found challenge generation deviating from the protocol spec and +recommended directly hashing the transcript with round indices for multiple +challenges. Linea showed how failed hash precompile calls could make challenges +predictable if return status is ignored. + +### 9. Match Halo2/KZG Opening Logic Exactly + +Audit the PCS layer separately: + +- KZG pairing equation sign and ordering. +- Whether `A`/opening accumulator is negated or not. +- SHPLONK accumulator construction. +- Multi-opening batching challenges. +- Non-empty accumulator/vector checks. +- Commitment/evaluation pairing: every evaluation must correspond to the + correct commitment and point. +- Rotations: `zeta`, `zeta * omega`, last-row rotations, wraparound handling. +- Lookup arguments and permutation arguments. +- Quotient polynomial chunk count and degree. +- Whether linearization polynomial evaluations are included in proof or + recomputed. +- Compatibility with the exact prover version. + +Linea found deviations from the intended PLONK approach, including mismatches +in custom gate commitments/evaluations and sign conventions, and recommended +thorough review of those sections. Axiom/Scroll found that native PCS deciders +accepting empty vectors could bypass verification, recommending non-empty +assertions and negative tests. + +### 10. Avoid Hardcoded One-Custom-Gate Or One-Commitment Assumptions + +Generated verifier templates often accidentally bake in the first supported +circuit. + +Check support for: + +- Zero, one, or many custom gate commitments. +- Zero, one, or many lookup arguments. +- Variable numbers of advice/fixed/instance columns. +- Multiple proof systems or aggregation depths. +- Empty arrays where invalid. +- Number of public inputs exactly matching the circuit. +- Proof layout generated from metadata, not hand-maintained constants. + +Linea found that its verifier supported only one BSB22 commitment and would +fail for zero or multiple commitments; the same report emphasized missing edge +case tests for no/multiple commitments. + +### 11. Make Memory And Yul Rules Explicit + +For a Solidity/Yul verifier: + +- Respect the free memory pointer. +- Do not clobber `0x40` or Solidity scratch conventions unexpectedly. +- Start verifier-owned memory at a known safe offset, usually `0x80`. +- Keep a memory map for state, transcript, scratch, precompile inputs, and + precompile outputs. +- Do not write computed values into `state_success` or other high-value flags + accidentally. +- Fail fast instead of carrying `state_success` through expensive later + operations. +- Remove debug writes and unused state fields. +- Use fixed compiler version and pinned solc binary. +- Regenerate ABI/bin in CI from source; do not commit stale artifacts. + +Axiom's Halo2 upgrade audit found that an EVM verifier ignored Solidity's free +memory pointer and recommended starting allocation at `0x80` and asserting +`mload(0x40) == 0x80`. Linea's formal verification appendix specifically +checked memory-state modification, state reset, and scratch-memory correctness, +which are good invariants to replicate. + +### 12. Error Behavior: Separate Malformed Input From Invalid Proof + +A production verifier should define: + +- Malformed calldata -> revert with custom error. +- Non-canonical field element -> revert. +- Invalid curve/subgroup point -> revert. +- Precompile failure -> revert. +- Wrong proof length -> revert. +- Mathematically well-formed but false proof -> either return `false` or + revert, but be consistent with the consuming protocol. +- Internal invariant violation -> revert. + +For general-purpose verifier APIs, prefer: + +```solidity +function verify(bytes calldata proof, bytes32[] calldata publicInputs) + external + view + returns (bool); +``` + +where malformed inputs revert and a well-formed invalid proof returns `false`. +For protocol entrypoints like `proveAndExecute(...)`, revert on both malformed +and invalid proof. Worldcoin's verifier audit recommended custom errors, and +the fix added distinct errors such as `PublicInputNotInField` and +`ProofInvalid`. + +### 13. Test Like A Malicious Prover, Not Just An Honest Prover + +Minimum negative test suite: + +- Valid reference proof from the canonical Halo2 prover. +- Every proof byte flipped one at a time. +- Every scalar replaced by `x + r`, `r`, `r - 1`, `0`, `1`. +- Every Fq coordinate replaced by `x + q`, `q`, `q - 1`, non-canonical + 48-byte values. +- G1/G2 infinity in every commitment slot. +- Off-curve points. +- Wrong-subgroup BLS points. +- Wrong proof length: short, long, extra trailing bytes. +- Wrong public input count. +- Public input not in Fr. +- Wrong endianness. +- Wrong VK digest. +- Wrong SRS element. +- Wrong domain size. +- `zeta` forced to domain point in test harness. +- Zero denominators in batch inversion. +- Precompile failure using mocks or a fork target that can simulate failure. +- Pairing precompile returns zero. +- Empty accumulator/MSM/vector. +- Multiple custom commitments and zero custom commitments. +- Differential tests against the Rust verifier. + +Linea explicitly called out missing edge-case tests for off-curve proof +elements, infinity, all-zero proof elements, wrong scalars, scalar wraparound, +invalid public inputs, and zero/multiple commitments. Scroll recommends +adversarial testing focused on malicious prover behavior, because ordinary +geth/reference tracing mostly exercises completeness rather than soundness. + +### 14. Use Lightweight Formal Methods For The Small Critical Helpers + +High-value targets for formal specs: + +- `load_fr`, `load_fq`, `load_g1`, `load_g2`. +- `is_canonical_fr`, `is_canonical_fq`. +- `batch_invert`. +- `invert_nonzero`. +- `evaluate_lagrange`. +- `evaluate_pi_poly`. +- `hash_to_field` / transcript challenge derivation. +- Precompile wrappers: success, returndata size, result semantics. +- Memory allocator / scratch memory separation. +- `state_success` monotonicity: once false, never true again. +- Calldata layout precheck. +- VK codehash/deployment precheck. +- Pairing equation wrapper. + +The Linea audit used Dafny/Z3 to prove selected verifier properties such as +termination, arithmetic overflow/underflow behavior, immutable inputs, +memory-state modification, state reset, scratch-memory safety, and batch +inversion correctness. + +### 15. BLS-Specific Implementation Notes + +Because this verifier uses BLS precompiles, add these BLS-only checks to the +BN254-derived lessons: + +- BLS Fq is 381 bits: Solidity `uint256` cannot hold a coordinate. Use + byte-sliced canonical comparisons. +- BLS Fr fits in 255 bits: scalar field arithmetic can use `addmod`/`mulmod` + with Fr modulus, but only after strict canonical Fr loading. +- G1 and G2 have cofactors: subgroup validation must be explicit or guaranteed + by the precompile spec. +- G2 encoding is easy to swap: lock down Fq2 coefficient ordering and test + against vectors from the exact off-chain library. +- Pairing precompile semantics must be known: does it reject invalid encodings, + return false, or fail the call? +- MSM precompile semantics must be known: empty MSM, infinity bases, zero + scalars, non-canonical scalars, subgroup checks. +- If proof points are compressed, implement decompression/sign handling once, + test it heavily, and expose a view/helper only if it cannot create + alternative encodings. Worldcoin's audit specifically reviewed compressed + proof support and recommended making complex compression helpers accessible + rather than leaving them only in tests. + +### 16. Code Generator Invariants + +Because Halo2 Solidity verifiers are usually generated, move as many checks as +possible into the generator: + +- Generate a machine-readable proof layout manifest. +- Generate Solidity offsets from that manifest, not hand-written constants. +- Generate tests from the same manifest. +- Generate static assertions for proof length, public input count, and VK + constants. +- Generate a reference JSON test vector: proof, public inputs, transcript + intermediate challenges, expected pairing inputs. +- Generate negative tests for every field/point input. +- Generate a memory map and assert no overlap. +- Generate NatSpec and custom errors. +- Pin solc version in generated output. +- Produce a reproducible build artifact and compare bytecode hash in CI. + +The Worldcoin audit notes that the verifier was produced by a Go data-driven +code generator, and its recommendations focused on template-level improvements +such as custom errors, compression helper availability, and NatSpec; those are +exactly the kinds of properties that should be baked into the generator once +rather than manually patched in each verifier. + +### Condensed Do-Not-Ship-Without List + +Do not ship until the verifier has: + +- strict proof length checks; +- canonical Fr/Fq loading; +- explicit G1/G2 on-curve/subgroup/infinity policy; +- all precompile success and return values checked; +- pairing result checked; +- zero inversion rejected; +- Lagrange root-of-unity case handled; +- VK/SRS/public inputs bound into transcript; +- Solidity memory pointer respected; +- invalid proof/invalid encoding tests; +- differential tests against the reference Halo2 verifier. diff --git a/proofs/solidity-verifier/docs/plans/SPLIT_NWAY_NOTES.md b/proofs/solidity-verifier/docs/plans/SPLIT_NWAY_NOTES.md new file mode 100644 index 000000000..1754667bb --- /dev/null +++ b/proofs/solidity-verifier/docs/plans/SPLIT_NWAY_NOTES.md @@ -0,0 +1,143 @@ +# Historical N-Way Quotient Helper Split — Session Notes + +These notes describe a previous helper-split experiment. They are not the +current public API reference. + +Summary of what was achieved this session. + +## Phase 1 — Inline-asm spill (replace `_vS`/`_vL` external calls) + +Each spilled assignment is now an inline `assembly { mload/mstore }` +block with per-statement load-temp locals. No per-call ~700 gas +overhead, optimizer can pack tightly. Smoke bytecode shrunk vs the +helper-call version. + +Each per-identity body now looks like: + +```solidity +{ + uint256 _l2; + uint256 _l3; + assembly { + _l2 := mload(add(vbase, 0x40)) + _l3 := mload(add(vbase, 0x60)) + } + uint256 _t = mulmod(_l2, _l3, r); + assembly { mstore(add(vbase, 0x80), _t) } +} +``` + +Slab is hand-allocated by bumping the free-memory pointer in inline +assembly — no `uint256[N] memory v` syntax (its generated +`zero_array_uint256_N_K_mpos` initializer itself overflows the Yul +stack at large N). + +## Phase 2 — N-way split + +Added: + +- `SolidityGenerator::render_with_quotient_helpers_n(N)` returning + `(main_sol, vk_sol, Vec)`. +- `emit_dispatch_block_n(sorted_simple, counts, mem_dump_size)`: + one `delegatecall` block per helper + one N-way sparse-Horner + combine block + tail mstore block. +- `helper_tag(i)` -> `"A".."Z"` for i<26, `"N{i}"` beyond. + +Identities are partitioned into N near-equal chunks (in identity +order). Main verifier dispatches via a `helpers_ptr`-rooted memory +array of helper addresses, then combines accumulators via N-way +sparse Horner. + +Template updates (`templates/contracts/Halo2Verifier.sol`): + +- Constructor signature changed to + `constructor(address authorizedVk, address[] memory helpers)`. +- N immutables `HELPER_0 .. HELPER_{N-1}` declared via + `{%- for i in 0..self.quotient_helpers_n %}` loop. +- `verifyProof` marshals the immutables into a stack-light + `address[N] memory __helpers`, then exposes the array-base + pointer to inline assembly via: + +```solidity +uint256 helpers_ptr; +assembly { helpers_ptr := __helpers } +``` + +so the QE dispatch loop reads each helper via +`mload(add(helpers_ptr, i*0x20))` (one slot for the base pointer, +zero stack pressure regardless of N). + +## Phase 3 — IVC e2e + +With `IVC_SPLIT_N=16`, all contracts fit under EIP-170 (24576-byte +cap): + +- main = 22 850 B (was 49 570 B inline) +- vk = 5 922 B +- helpers (compiled bytecode): + + ``` + A=1392 B=737 C=5096 D=10214 E=17923 F=23459 G=23545 H=7584 + I=762 J=4143 K=3444 L=3579 M=5000 N=175 O=175 P=175 + ``` + + max 23 545 B, all under the 24 576-byte cap. + +The split path deploys and runs end-to-end on Prague-spec revm. The +verifier hits the same pre-existing gas-bounded revert as the inline +path (~4.92 B gas with a 5 B cap), confirming the split is +logic-equivalent. The remaining revert is an independent, documented +IVC-specific issue (likely missing accumulator encoding configuration / +acc-pairing logic, or a precompile-failure cascade), not related to the +helper split. + +Inline vs split `gas_used` deltas: + +``` +Inline 4 922 038 815 -> Split 4 922 038 334 (-481 gas) +``` + +The 481-gas difference reflects the QE block becoming a delegatecall +chain that's slightly cheaper than the inlined Yul block, and is +within the noise budget of solc's optimizer rearrangements. + +## Files touched + +- `src/codegen.rs` + - `render_with_quotient_helpers()` -> thin wrapper over + `render_with_quotient_helpers_n(2)`. + - `render_with_quotient_helpers_n(N)` -> new public API. + - `emit_dispatch_block_n(sorted_simple, counts, mem_dump_size)`. + - `helper_tag(i)`. + - split quotient helper rendering with per-identity inline-asm spill + (replaces `_vS`/`_vL` external calls). + - `collect_named_refs()` helper. +- `src/codegen/template.rs` + - `Halo2Verifier.quotient_helpers_n: usize` field. +- `src/evm.rs` + - `Evm::create_with_address_and_address_array_arg(...)` to deploy + the new `(address, address[] memory)` constructor. +- `src/lib.rs` — re-exports. +- `templates/contracts/Halo2Verifier.sol` + - Per-helper immutables loop. + - `address[] memory helpers` constructor. + - Stack-light `__helpers` array + `helpers_ptr` for assembly. +- `tests/ivc_keccak_solidity.rs` + - `IVC_SPLIT_N` env var (default 16). + - Renders + compiles + deploys all helpers + main with + `create_with_address_and_address_array_arg`. + - Per-helper EIP-170 size check + summary line. +- `src/test.rs` — `quotient_split_renders_and_compiles` smoke test + unchanged (still calls 2-arg `render_with_quotient_helpers`). + +## Known limits / next steps + +1. Underlying IVC verifier revert at ~4.92 B gas is pre-existing and + unrelated to the helper split; chasing it down probably needs + accumulator encoding configuration plumbed through and/or a + precompile-fail audit. +2. With N=16 the largest helper is right at 23.5 KB. A circuit larger + than the IVC could push helpers over 24 KB; bump `IVC_SPLIT_N` or + add a size-aware partitioner if that becomes an issue. +3. Per-helper deployment cost is ~50 KB code-deposit gas × 16 = ~800 KB + total deployment bytecode. One-time cost; verifyProof is unaffected. diff --git a/proofs/solidity-verifier/docs/plans/TESTING_STRATEGY.md b/proofs/solidity-verifier/docs/plans/TESTING_STRATEGY.md new file mode 100644 index 000000000..b92a2215f --- /dev/null +++ b/proofs/solidity-verifier/docs/plans/TESTING_STRATEGY.md @@ -0,0 +1,2589 @@ +# Testing Strategy + +This document records the verifier-bug taxonomy from the source prompt and +turns it into a practical test suite for this repository's Halo2 Solidity +verifier generator. + +The core target is generated on-chain verifier code for Midnight/Halo2 KZG on +BLS12-381, including embedded verifier-key mode, separate verifier-key mode, +pinned external quotient evaluators, EIP-2537 precompile integration, calldata +parsing, public-input handling, transcript equivalence, accumulator handling, +and wrapper/application binding. + +## Source Prompt Taxonomy + +The prompt asked for a test suite based on a practical taxonomy of +SNARK/STARK verifier contract bugs. The consolidated taxonomy below preserves +the prompt's main audit concerns and maps them to testable invariants. + +### 1. Statement-Binding Bugs + +These bugs let the proof verify a statement different from the one the contract +thinks it is verifying. + +- Public input not fully bound: every protocol-critical value must be included + in public inputs or the verifier transcript. +- Wrong public-input ordering: compare generated verifier ABI, Solidity + packing, and circuit witness/public-input layout. +- Missing public input length check: enforce the exact length in the base + verifier, not only in wrappers. +- Non-canonical public inputs: reject values greater than or equal to the field + modulus before hashing, events, storage, or verification. +- Unused public inputs: require every non-zero public input entry to be + referenced by active proof logic. +- Statement not bound to action: wrappers must bind chain, contract, caller, + recipient, nullifier, amount, root, image ID, journal digest, calldata hash, + and other application-specific context. + +### 2. Setup, Verifying-Key, Circuit-Identity, and Parameter Trust Bugs + +The proof may be valid for a different circuit, verifier key, version, or +parameter set than intended. + +- Toxic-waste or CRS misuse: deployed verifiers must not rely on toy or + unverifiable setup parameters. +- Verifying-key mismatch: bind domain size, public-input count, custom gate + set, commitment layout, circuit digest, and verifier-key code. +- Wrong VK selected: registry keys must identify the full circuit shape and + version, not only partial parameters. +- Missing VK/dependency binding: pin VK contract codehash, quotient evaluator + codehash, preprocessed roots, SRS elements, curve, and circuit digest. +- VK upgrade risk: verifier upgrades should be timelocked, immutable for + emergency exits, or paired with user exit windows. +- Missing deployment-code validation: hash or validate deployed bytecode and + generated constants. +- Recursive verifier wrong inner VK: inner VK must be constant or transcript + bound and impossible to swap. +- Wrong security parameters: FRI query count, blowup factor, grinding, + commitment security bits, and extension-field assumptions must be on-chain + or otherwise pinned. + +### 3. ABI, Calldata, Encoding, and Memory-Layout Bugs + +The wrapper and verifier parse different proof or instance data, or low-level +code corrupts memory. + +- Non-canonical ABI accepted: assert exact dynamic offsets, lengths, and + trailing data, or use Solidity ABI decoding directly. +- Proof selector not checked: validate selector and expected verifier entry + point. +- Proof length/layout bugs: reject truncation, extra bytes, calldata overlap, + stale bytes, off-by-one unmarshalling, and hardcoded proof-size drift. +- Endianness and serialization drift: test off-chain/on-chain byte order, + point compression conventions, and field canonicalization. +- Free memory pointer clobber: assembly must respect Solidity memory layout, + use planned scratch, and avoid reserved memory corruption. +- Return length mismatch: precompile/verifier returns must match documented + ABI exactly. +- Out-of-bounds memory reads: failed reads must not turn into zeroes or stale + data. +- Magic offsets/constants: generated layouts should be derived from typed + metadata and checked by tests. + +### 4. Field-Element Canonicalization and Range Bugs + +These bugs arise when EVM `uint256`, native integers, limbs, and finite-field +elements are confused. + +- Missing `< q` or `< r` checks: public inputs, proof scalars, and point + coordinates must be canonical before use. +- Non-canonical public inputs: reject `x + r` or `x + q` encodings instead of + reducing them silently. +- Packing and width bugs: packed small-field values must have strict width and + unused-bit checks. +- Modulus wraparound: attested or computed data must not wrap modulo field. +- Non-native limb range unsoundness: prove limb range and native congruence. +- Truncation: reject 512-bit to 256-bit overflow or panic paths. +- Debug-only validation: security checks must be runtime assertions or circuit + constraints, not `debug_assert!`. +- Zero inverse/division edge cases: reject denominator zero unless the protocol + explicitly defines that behavior. + +### 5. Curve, Pairing, Subgroup, and Point-Encoding Bugs + +These are common in Groth16, KZG, IPA, Halo2, BLS, and pairing precompiles. + +- Missing curve/subgroup checks: proof commitments, VK commitments, G1/G2 + points, and accumulators must be valid subgroup elements. +- Identity point accepted: reject identity public keys, accumulators, and proof + commitments unless explicitly supported. +- Point-at-infinity mishandled: define one representation and handle it in + every EC operation. +- Invalid point deserialization: reject coordinates not in field or not on + curve before pairing, MSM, or scalar multiplication. +- Non-canonical point coordinates: reject `x >= p`, `y >= p`, or multiple + encodings of one point. +- Unsound point negation: validate `y < q` and on-curve before computing + `q - y`. +- Scalar range omission: reject proof scalars outside the scalar field where + required. +- Pairing equation implementation bugs: test sign, negation, term order, and + constants against trusted vectors. +- Precompile failure conflated with invalid proof: distinguish malformed + inputs, verifier-key dependency failures, and semantic proof failure where + the API supports it. + +### 6. Fiat-Shamir Transcript Bugs + +Non-interactive verification must bind every part of the statement and every +prover message before deriving challenges. + +- Missing transcript fields: VK, public inputs, commitments, domains, + protocol IDs, curve parameters, SRS elements, circuit IDs, and public params + must be absorbed. +- Missing prover/public data: public memory, LogUp sums, commitment roots, and + proof metadata must be challenge-bound. +- Wrong domain separation/order: use explicit domain tags and round labels per + proof type, chain, circuit, and verifier. +- Public inputs not included in transcript: challenge generation must include + public inputs when the protocol requires it. +- Challenge bias: avoid biased modulo reduction unless documented by the + proof system; use the native verifier as the oracle. +- Hash domain collisions: prefix lengths, node types, proof/data domains, and + variable-length structures. +- Failed hash/precompile calls not checked: failed SHA/modexp/staticcall + paths must not leave stale memory that becomes a challenge. + +### 7. Polynomial-Commitment, Accumulation, Pairing, IPA, KZG, and FRI Bugs + +This layer is often the final check. A single missing boolean can invalidate +the verifier. + +- Pairing result ignored: check both precompile call success and semantic + result. +- Empty accumulator accepted: reject empty proof, accumulator, PCS, KZG, IPA, + or FRI vectors unless explicitly valid. +- Commitment not checked: validate proof commitments against configured or + attested commitments. +- Missing commitment opening check: every required opening must be checked at + the correct point and against the correct commitment. +- All-but-pairing proof not finalized: final pairing/decision must always run. +- Aggregation spec incomplete: outer proof must imply all inner proofs, bind + inner VK constants, and bind accumulator state. +- Wrong commitment opening domain: differential test against the native + verifier. +- Merkle/FRI decommitment skip: prover-controlled lengths must not skip + iterations or checks. +- Preprocessed trace/root unchecked: STARK preprocessed roots and trace roots + must be pinned. + +### 8. Polynomial Identity and Domain Edge-Case Bugs + +Verifier math is often correct except at boundary cases. + +- Root-of-unity special cases: Lagrange evaluation must be correct when + challenge points lie in the evaluation domain. +- Vanishing polynomial edge cases: avoid division by zero at domain points. +- Batch inversion with zeros: handle empty, singleton, and zero-containing + ranges. +- Wrong polynomial version: do not mix old/new PLONK linearization formulas, + signs, constants, quotient shard degrees, or domain sizes. +- Domain-size miscalculation: check `n`, `m`, `k`, public-input counts, and + number of constraints. + +### 9. Underconstrained Circuit and Recursive-Verifier Bugs + +Even with a correct Solidity verifier, an underconstrained circuit can make +false statements verify. + +- Missing equality constraint: negative tests with malicious witnesses. +- Missing boolean/range constraint: selectors, indices, flags, signs, bytes, + tags, and condition flags must be constrained to valid domains. +- Non-unique witness: prove uniqueness or specify tie-breaking. +- Lookup misuse: membership is not multiplicity or permutation. +- Zero division/inverse unconstrained: require denominator nonzero or define + zero behavior. +- Unused verifier output: recursive verifier circuits must constrain the + verification result to true and expose/bind the exact public input consumed + by the outer verifier. +- Underconstrained VM/opcode semantics: malicious prover behavior should be + tested against reference semantics. +- Error/success path overlap: success and error gadgets must be disjoint. +- Release-build-only underconstraints: no security-critical `debug_assert!` + paths. + +### 10. STARK-Specific Verifier Bugs + +For STARK contracts, equivalent bugs usually live in FRI, Merkle, AIR, and +transcript logic. + +- AIR public input mismatch: bind trace length, program hash, memory roots, + public memory, and output roots. +- FRI parameter mismatch: enforce blowup factor, folding schedule, query + count, domain size, and proof-of-work on-chain. +- Query sampling bias: derive query indices from the complete transcript. +- Merkle path malleability: domain-separate leaves/nodes and bind level, + index, and path length. +- Field extension encoding bugs: test limb order and encoding against + canonical vectors. +- OOD/DEEP composition mistakes: bind out-of-domain challenges and check the + exact composition polynomial formula. +- Proof-of-work grinding mistakes: verify exact transcript prefix and target. + +### 11. Protocol Integration, App Binding, Replay, and Liveness Bugs + +The proof may be true while the surrounding protocol still fails. + +- Proof valid but state transition unusable: test rollover, max IDs, max + roots, and long-running counters. +- Forced-exit verifier can be disabled: model censorship and forced withdrawal + paths. +- Nullifiers not reserved: delayed proofs can be invalidated by spending the + same nullifier. +- Snapshot timing races: version roots and bind proofs to snapshot versions. +- Unspendable outputs: commitment and encrypted note metadata must match. +- Replay/malleability misuse: do not use Groth16 or other malleable proof + bytes as unique IDs unless the design accounts for malleability. +- Emergency stop too narrow: kill-switches should cover generalized proof of + exploitation, not only one hardcoded invalid statement. +- Cross-layer finality and root freshness: bind chain roots and fraud-window + assumptions. +- Upgradable verifier/router risk: pin dependency codehashes and verifier + router targets. + +### 12. Fail-Open, Fail-Closed, DoS, and Low-Level EVM Bugs + +Low-level integration bugs can break otherwise correct cryptography. + +- Panic on malformed proof: malformed input should fail fast and clearly. +- Fail-open assembly return: helpers must not use `return(0, 0)` in a way that + exits verification successfully. +- Malformed proof causes expensive path first: validate lengths, offsets, + bounds, and field elements before hashing or precompiles. +- Valid proofs rejected: positive vectors and interoperability tests should + catch over-strict canonicality and wrong return formats. +- Error conflation: custom errors or documented revert policy should + distinguish dependency failures from invalid proofs where useful. +- `staticcall` status ignored: ECADD, ECMUL, pairing, SHA, and modexp failures + must not leave stale memory. +- Return buffer stale data: failed precompile return data must not be reused. +- Gas subtraction griefing: avoid `gas() - constant` patterns that fail near + the end of execution. +- ABI/bin/template drift: committed artifacts must be regenerated from current + templates and compiler settings. + +### 13. Testing and Specification Gaps + +The prompt emphasized that missing negative tests and missing specifications +are strong predictors of verifier bugs. + +- Only positive proof tests miss ignored pairing results, malleability, missing + length/range checks, invalid points, and invalid public inputs. +- No malformed calldata tests misses truncation, overlap, bad offsets, and + trailing data. +- No adversarial prover tests misses underconstrained witnesses and skipped + Merkle paths. +- No differential tests misses native/EVM divergence. +- No known-answer vectors misses hash, Keccak, pairing, FRI, EC, and + serialization mistakes. +- Spec-code drift: comments and docs should be treated as formal layout/API + specifications. +- Generator bugs: audit templates and generators, not only generated + instances. + +## Highest-Risk Prompt Checklist + +For each generated verifier, the prompt's highest-risk invariants become: + +1. The verifier proves the intended circuit: VK, circuit version, shape, public + input length, and proof-system parameters are all bound. +2. The contract computes and consumes exactly the same public inputs as the + circuit expects. +3. Every public input is canonical, field-bounded, ordered, and consumed. +4. ABI/calldata decoding is canonical; no alternate encoding can make wrapper + and verifier parse different data. +5. All EC points, commitments, and accumulator elements are on-curve, + in-subgroup, non-identity when required, and correctly encoded. +6. The transcript includes all commitments, public inputs, VK/domain data, and + protocol separators. +7. Precompile calls check both call success and expected return size/result. +8. Assembly respects Solidity memory layout and never fails open. +9. Upgrade/admin controls cannot silently replace, weaken, or disable the + verifier. +10. Negative tests exist for malformed proofs, wrong public inputs, wrong VK, + wrong proof length, non-canonical field elements, empty accumulators, and + replay/liveness edge cases. + +## Repo-Specific Testing Goal + +Build a verifier conformance and adversarial regression suite, mostly in +Rust/revm, because this repository already has that harness in `src/test.rs`, +with slow IVC coverage in `tests/ivc_keccak_solidity.rs`. + +Every generated verifier should pass three oracles: + +1. Native Midfall/Halo2 verification accepts the valid proof. +2. Solidity/revm accepts exactly the same proof, public inputs, VK, quotient + evaluator, and feature profile. +3. Every targeted mutation either reverts or fails, never returns true. + +## Fixed-Artifact Correctness Strategy + +For a single production circuit, do not claim that the generator is correct for +all possible circuits. Claim correctness of the specific generated verifier +artifact under review. + +The clean two-way claim is: + +```text +For this fixed circuit, fixed VK, fixed Solidity source/bytecode, fixed compiler +settings, and documented calldata encoding, verifyProof(proof, instances) +returns true if and only if the Rust Halo2/Midfall verifier accepts the +corresponding proof and instances. +``` + +For on-chain security, the most important claim is the one-way no-false-accepts +property: + +```text +If the Solidity verifier returns true, then the Rust verifier would accept the +same mathematical proof for the same VK and public inputs. +``` + +### Artifact Manifest + +Freeze and record every artifact input that affects verification: + +- Generated Solidity file hash. +- Deployed runtime bytecode hash. +- Solidity compiler version. +- Optimizer settings and EVM target. +- VK hash and embedded constant digest. +- External VK bytecode hash, if used. +- External quotient evaluator bytecode hash, if used. +- Exact ABI: `verifyProof(bytes proof, uint256[] instances)`. + +This makes the proof about a concrete verifier artifact, not about the +generator as a whole. + +### Decoding Relation + +Document how calldata maps to mathematical verifier objects and Rust verifier +objects: + +| Solidity input | Mathematical object | Rust equivalent | +| --- | --- | --- | +| `uint256` scalar word | Element of `Fr`, checked `< r` | `Scalar` | +| G1 coordinates | Affine BLS12-381 point, checked on curve/subgroup as required | Commitment or proof point | +| Public input words | Instance field values | `instances` | +| Embedded constants | Fixed VK commitments and parameters | `VerifyingKey` | + +This relation is the bridge between Solidity ABI bytes and Rust verifier types; +it should explicitly cover endianness, field modulus checks, point encoding, +dynamic ABI heads, and committed versus uncommitted instance layout. + +### Parser Equivalence + +Prove every `calldataload` offset reads the same proof field that Rust reads, in +the same order. Required checks: + +- Scalars satisfy `0 <= a < r`. +- Commitment coordinates are canonical and valid before use, with subgroup + requirements documented at the exact EIP-2537 operation that enforces them. +- Calldata length is exact. +- Trailing bytes are rejected. +- Dynamic ABI regions do not overlap and have canonical offsets. +- No unused calldata word can change transcript or PCS semantics while escaping + validation. + +### Transcript Equivalence + +Show Solidity absorbs the same mathematical objects in the same order as Rust. +Then prove every challenge agrees: + +```text +theta_sol = theta_rust +beta_sol = beta_rust +gamma_sol = gamma_rust +y_sol = y_rust +x_sol = x_rust +x1..x4_sol = x1..x4_rust +``` + +This is usually the highest-risk area, so trace it byte-for-byte. The trace +should identify the absorbed object, its encoded bytes, and the resulting +challenge for each transcript round. + +### Quotient Numerator Equivalence + +For the fixed circuit, write the exact generated quotient numerator: + +```text +E(x) = sum_i y^i e_i(x) +``` + +where each `e_i` is the concrete gate, permutation, lookup, and trash identity +for this circuit. Then show the Solidity evaluator computes the same `E(x)` +using the same advice, fixed, instance, and rotated evaluations: + +```text +a_j(omega^r x) +``` + +For one circuit, this can be fully explicit. The argument does not need to cover +all possible gates or all possible generator outputs. + +### Quotient and Linearization Equivalence + +The Rust verifier checks, in linearized form: + +```text +E(x) = H(x)(x^n - 1) +``` + +with quotient chunks recombined as: + +```text +H(x) = sum_k x^(k(n - 1)) H_k(x) +``` + +Show the Solidity artifact computes the same scalar side and the same +commitment side, including the exact selector folds, quotient chunk order, +rotation powers, and any external quotient evaluator return values. + +### KZG Check Equivalence + +Show the Solidity PCS batching constructs the same final KZG opening equation +as Rust. In essence, it must check: + +```text +C_star - v_star G1 = (s - x3) pi +``` + +via the pairing equation: + +```text +e(pi, [s]G2) * e(C_star - v_star G1 + x3 pi, -G2) = 1 +``` + +For the fixed artifact, prove Solidity computes the same `C_star`, `v_star`, +and `pi` as Rust. + +### Differential Trace Evidence + +Back the proof with paired Rust and Solidity traces. They should compare: + +- Transcript challenges. +- Quotient numerator. +- Selector folds. +- Linearization scalar. +- PCS intermediate values. +- Final pairing inputs. +- Valid proofs accepted by both verifiers. +- Mutated proofs rejected by both verifiers, or rejected earlier by stricter + Solidity artifact validation. + +The intended framing for review is: + +```text +We are not relying on the generator being correct for all circuits. For this one +circuit, we prove the emitted verifier artifact refines the Rust verifier stage +by stage. The Solidity parser decodes the same proof object, the transcript +derives the same challenges, the quotient evaluator computes the same batched +PLONK numerator, and the PCS code checks the same KZG pairing equation. +Differential traces compare the Rust and Solidity executions at each semantic +checkpoint. +``` + +## Proposed Test Suite + +| Layer | Tests | +| --- | --- | +| Positive vectors | Valid Poseidon fixture, shape-fuzz circuits, and IVC final Keccak proof must verify in embedded VK, separate VK, and pinned quotient modes. | +| Public input binding | Mutate every public input slot, not only slot 0. Swap instance order, truncate/extend instances, set each input to `Fr`, `Fr + 1`, `2Fr - 1`, and for IVC mutate every accumulator limb word. | +| ABI/calldata canonicality | Wrong selector, empty proof, truncated proof, trailing bytes, overlapping dynamic heads, shifted but otherwise ABI-valid heads, wrong length words, stale padding between proof and instances. | +| VK/circuit identity | Mutate each VK section: digest, params, fixed commitments, permutation commitments, quotient constants, quotient program. Verify constructor rejects wrong VK codehash, wrong quotient codehash, wrong runtime length, swapped VK/quotient addresses. | +| Proof scalar canonicality | For every scalar offset in the repacked proof, test `Fr`, `Fr + 1`, high-bit values, and random noncanonical words. These should reject before or during verification. | +| G1/EIP-2537 encoding | For every proof G1: nonzero top padding, coordinate `p`, coordinate `p + 1`, off-curve point, infinity where not explicitly allowed, and on-curve wrong-subgroup point if a fixture can be generated. | +| Transcript equivalence | Trace Rust and Solidity for all challenge stages: VK digest, committed instance, public instances, advice commitments, theta/beta/gamma/y/x/x1/x2/x3/x4, quotient eval, PCS inputs, final pairing inputs. Keep the native/Solidity trace comparison as a required EVM gate. | +| Empty/edge circuit shapes | Shape fuzz circuits with no advice in a phase, no lookups, one lookup, additive selectors, complex selectors, next rotations, second phase advice, permutation on/off, and wide advice counts that stress memory layout. | +| PCS/KZG/quotient | Mutate every quotient commitment, proof eval, opening proof, batching scalar source, quotient evaluator output, and external quotient return length. Assert the final pairing result is semantically checked, not just precompile call success. | +| Accumulator-specific | Check accumulator schema consumes exactly the expected public input words. Test unused high limb bits, malformed identity encoding, x/y limb swaps, scalar mutation, zero/identity accumulator cases, and any future fixed-base tail. | +| Precompile/fail behavior | Constructor smoke tests for EIP-2537 are good; add tests for short return data, false pairing result, reverted precompile call, and stale return memory using generated-template mutations or a helper harness. | +| Memory/layout | Fast generator tests should assert no overlap between VK, challenge, transcript, quotient, PCS, accumulator, and scratch regions. Keep these as compile-time/layout tests in `src/codegen/mod.rs` and `src/codegen/template.rs`. | +| Production artifact checks | `verifyProof` production renders stay `external view`, no `LOG1`, no gas checkpoints, Solidity pragma `^0.8.24`, Cancun/Prague target, runtime size below EIP-170 with margin. | +| Wrapper/application binding | Add small mock wrapper contracts that bind expected state root, program ID, chain/domain, caller/action hash, nullifier/nonce. Same proof with wrong wrapper context must reject. | + +## Priority Backlog + +### P0 + +- Mutate every public input slot. +- Expand ABI canonicality tests to all verifier variants: embedded VK, + separate VK, pinned quotient, trace, and gas-checkpoint render paths where + applicable. +- Add category-aware proof mutations for every scalar, G1, eval, commitment, + and opening offset. +- Require native/Solidity trace equivalence in the EVM gate whenever the + `rust-verifier-trace` feature is available. + +### P1 + +- Add accumulator malformation tests outside the slow IVC bench, especially + unused high limb bits and identity encoding variants. +- Add precompile-return semantic tests: short return, false pairing return, + reverted call, stale memory, and bounded-gas failure. +- Add quotient-evaluator adversarial tests for wrong output length, wrong + codehash, wrong runtime length, mutated quotient program, and mutated + quotient constants. +- Add field-boundary tests for `Fr`, `Fr + 1`, `2Fr - 1`, `p`, `p + 1`, and + high-bit values where the encoding makes sense. + +### P2 + +- Add wrapper-level tests for application binding and replay/nullifier + behavior, since raw `verifyProof(bytes,uint256[])` intentionally proves only + "this proof verifies for these public instances under this VK." +- Add generated layout invariant tests for every rendered constant group and + memory region. +- Add wrong-subgroup G1/G2 fixtures if a reliable generator can produce them. +- Add long-running/liveness tests for counters, root windows, and snapshot + versions in downstream integration wrappers. + +## Run Tiers + +### Fast CI + +```bash +cargo test --workspace --all-features --all-targets +``` + +### EVM Negative Suite + +```bash +HALO2_SOLIDITY_RUN_EVM_TESTS=1 \ +cargo test --release --features evm,truncated-challenges,rust-verifier-trace -- --nocapture +``` + +### Property-Based EVM Suite + +```bash +cargo test --release --all-features pbt_ -- --ignored --nocapture +``` + +### Slow IVC/Accumulator Suite + +```bash +HALO2_SOLIDITY_RUN_IVC_BENCH=1 \ +cargo test --release --features evm,truncated-challenges,fewer-point-sets,rust-verifier-trace \ + --test ivc_keccak_solidity -- --nocapture +``` + +### Full Local Stress Run + +```bash +cargo test --release --workspace --all-features --all-targets \ + -- --include-ignored --nocapture +``` + +## Implementation Mapping + +Existing coverage already includes a healthy baseline: + +- Positive Poseidon fixture verification. +- Public-input mutation for basic cases. +- Proof bit-flip rejection. +- Separate VK pinning and VK payload mutation. +- Pinned quotient dependency checks. +- Malformed calldata rejection. +- EIP-2537 constructor smoke tests. +- Production render checks for `external view` and no gas logs. +- Native/Solidity trace equivalence. +- Scalar canonicality tests for proof scalars. +- Noncanonical and off-curve G1 rejection. +- Slow IVC accumulator packing rejection in the bench path. + +The highest-value additions are: + +- Broader per-slot public-input mutations. +- Lighter accumulator canonicality fixtures that do not require the full IVC + bench. +- Precompile failure and return-size harnesses. +- Category-aware proof-layout mutation helpers. +- Wrapper tests that bind application-specific state. + +## Definition of Done + +A verifier profile is considered covered when: + +1. At least one valid native proof verifies in every supported generated + Solidity mode for that profile. +2. Native and Solidity trace outputs match for the transcript, quotient, PCS, + and final pairing checkpoints exposed by the profile. +3. Every declared proof scalar, G1 point, public-input word, VK section, + quotient section, and accumulator word has at least one negative mutation + test. +4. ABI canonicality tests reject alternate but Solidity-decodable calldata + forms that the hand-rolled parser is not intended to accept. +5. EIP-2537 integration tests cover call failure, short return, semantic false + return, and valid precompile behavior. +6. Production artifacts compile with the pinned compiler/EVM target and stay + within size limits. +7. Raw verifier NatSpec documents that application contracts must bind + protocol semantics, and wrapper tests prove those bindings reject replay or + wrong-context proofs. + +## Layered Verifier-Assurance Addendum + +The repo-specific strategy above should be implemented as a layered assurance +suite: + +- Cheap deterministic tests on every PR. +- Fuzz and property tests nightly. +- Mutation testing weekly and before release. +- Small formal models for the highest-risk parser, transcript, state, and + precompile boundaries. + +The core invariant for every verifier integration is: + +```text +For any accepted proof, the contract's interpretation of the statement must +equal the circuit/protocol's interpretation of the statement, and all +protocol-critical state transitions must remain live under long-running usage. +``` + +This catches recurring audit failures: public-input mismatches, +non-canonical inputs, wrong VK selection, subgroup mistakes, verifier upgrade +risks, rollovers, and underconstrained circuit helpers. PrivacyBoost's +tree-number issue is the canonical liveness example: the circuit constrained +tree numbers as small sparse-array indices while the contract treated them as +15-bit global identifiers, eventually halting deposits, transfers, +withdrawals, and forced withdrawals after enough rollovers. + +### Deterministic Negative Tests: Invalid Things Must Fail + +Create a reusable negative suite, for example `VerifierNegative.rs` for this +repo and `VerifierNegative.t.sol` for downstream Solidity wrappers. + +#### Proof And Public Input Mutations + +For every valid proof fixture: + +| Mutation | Expected result | +| --- | --- | +| Flip every byte of proof once. | Reject. | +| Flip every public input once. | Reject. | +| Replace a public input with `x + q`. | Reject or canonicalize consistently. | +| Add extra public inputs. | Reject. | +| Remove one public input. | Reject. | +| Reorder public inputs. | Reject. | +| Replace a used root with known-but-unused root. | Reject if canonical encoding is required. | +| Add trailing unused calldata words. | Reject. | +| Use duplicate `(treeNumber, root)` pairs. | Reject unless explicitly allowed. | +| Use wrong verifier/VK for same public-input count. | Reject. | + +This targets bugs like missing base verifier length checks, where +`publicInputs.length + 1 == vk.icLen` should be enforced in the base verifier +itself, not only in callers. It also targets non-canonical commitments such as +`x` and `x + q` hashing to the same field element while events and relayer +tooling see different calldata. + +#### EC And Pairing Edge Cases + +Add fixtures for: + +| Input | Expected result | +| --- | --- | +| G1/G2 point not on curve. | Reject. | +| G2 point not in subgroup. | Reject or prove impossible by circuit-level constraints. | +| Identity point where not allowed. | Reject. | +| Point at infinity in every EC operation path. | Correctly handled or rejected. | +| Zero denominator or inverse-of-zero path. | Reject or document defined behavior. | +| Scalars equal to `0`, `1`, `r-1`, `r`, `r+1`, `2^256-1`. | Correct accept/reject. | + +This suite targets findings such as missing subgroup checks for G2 points, +lack of zero checks in inverse computation, and debug-only assertions that +disappear in release mode. + +### Differential Tests: Native Verifier Versus Solidity/Yul + +For each proof system, keep a reference verifier in the language where the +proof was generated. + +For every generated fixture: + +```text +nativeVerify(vk, proof, publicInputs) + == solidityVerify(vk, proof, publicInputs) +``` + +Run this over: + +1. Valid fixtures generated by the prover. +2. Invalid fixtures generated by mutating proof bytes. +3. Invalid fixtures generated by mutating public inputs. +4. Boundary fixtures for field/scalar values. +5. ABI-malformed fixtures. + +This catches ABI packing errors, endianness mistakes, point encoding +differences, precompile return-length mismatches, and precompile return-value +mismatches. + +For precompiles or low-level libraries, add differential tests against +known-good implementations: + +```text +evmPairing(input) == arkworksPairing(input) +evmModExp(input) == bigintReferenceModExp(input) +solidityPoseidon(input) == nativePoseidon(input) +publicInputBuilderSolidity(tx) == publicInputBuilderRust(tx) +``` + +Worldcoin's Groth16 verifier audit is an example of this layer: optimized +Solidity assembly, compressed proof handling, constants, and pairing +precompile usage are exactly where differential tests pay off. + +### Property Fuzzing + +Use Foundry invariant tests for Solidity wrappers and `cargo-fuzz` or +`proptest` for Rust/native verifier code. + +#### Solidity Fuzz Harness + +Expose a harness with helpers such as: + +```solidity +function tryVerify(bytes calldata proof, uint256[] calldata inputs) external returns (bool); +function buildPublicInputs(Request calldata r) external view returns (uint256[] memory); +function verifyAndApply(Request calldata r, bytes calldata proof) external; +``` + +Fuzz these properties: + +| Property | Example assertion | +| --- | --- | +| No malformed input panics unexpectedly. | Only expected custom errors. | +| No accepted public input is `>= field modulus`. | `forall pi: pi < q`. | +| No accepted wrong-length public inputs. | `inputs.length == vk.icLen - 1`. | +| Canonical calldata only. | Trailing, duplicate, and unused data rejected. | +| Verifier failure has correct error class. | Field error differs from precompile failure where the API exposes that. | +| Upgrade cannot disable forced exit immediately. | Timelock or immutability invariant. | + +#### Long-Horizon State Fuzzing + +Run randomized action sequences as invariant tests: + +```text +deposit -> epoch -> transfer -> withdrawal -> rollover + -> forced withdrawal -> cancel -> upgrade attempt +``` + +Important invariants: + +| Invariant | Why | +| --- | --- | +| After N rollovers, proofs are still constructible for all supported flows. | Catches tree-number/domain-size mismatches. | +| Forced withdrawal remains executable under relay censorship. | Catches exit-liveness bugs. | +| Revoked keys stop working at the intended boundary. | Catches lazy snapshot/key-race bugs. | +| Verifier upgrades cannot immediately brick exits. | Catches governance liveness risk. | +| Nullifiers cannot be both pending-forced-exit and spent elsewhere. | Catches relay race issues. | + +PrivacyBoost had multiple liveness-adjacent issues around forced withdrawals, +verifier upgrades, requester-only execution, lazy auth snapshots, and +nullifier spending during the forced-withdrawal delay. + +### Circuit-Level Negative Tests + +For every reusable circuit gadget, add positive and negative tests. + +| Gadget | Negative tests | +| --- | --- | +| `assert_equal` | Two unequal values must fail. | +| `is_zero` / inverse | `(0,0)` division must fail unless defined. | +| range check | `max`, `max+1`, `q-1`, `q`, `q+1`. | +| `num_to_bits` | Reconstruct bits to original value. | +| selector / one-hot | All-zero and multi-one vectors fail. | +| lookup / shuffle | Wrong multiplicity fails. | +| EC load | Off-curve and infinity cases fail. | +| scalar multiplication | Unreduced scalar fails or reduces explicitly. | +| KZG/IPA accumulator | Empty accumulator vector fails. | + +Axiom's Halo2 audit is a strong template: it found underconstrained circuits, +debug-only validations, `0/0` division, point-at-infinity issues, non-reduced +field elements, empty KZG accumulators, and even `assert_equal` comparing a +value to itself. Basic unit tests and negative tests would have prevented +several of those findings. + +For proof libraries, mirror Anza's recommendation: every `verify()` path needs +a test where the final verification equation fails. Without that negative +test, a suite may not detect an implementation that skips the final check. + +### Metamorphic Canonicality Tests + +These tests are especially useful for SNARK/STARK verifier wrappers: + +```text +verify(proof, inputs) == false +for inputs' where inputs'[i] = inputs[i] + q + +hashPublicInputs(inputs) != hashPublicInputs(inputs with trailing zeros) + +buildPublicInputs(request) == buildPublicInputs(decode(encode(request))) + +decode(encode(x)) == x +encode(decode(bytes)) == bytes only for canonical bytes +``` + +For Merkle, STARK, and STARK-like proof systems: + +```text +leafHash(x) must never equal branchHash(y, z) +hash([a,b,c]) must not equal hash([hash(a,b),c]) +inclusionProof(k) and nonInclusionProof(k) cannot both verify under same root +``` + +Scroll's zkTrie audit is a good example: missing leaf/branch domain +separation enabled proof forgery, including contradictory inclusion and +non-inclusion proofs under the same root. It also found missing proof +validation that could crash the verifier, and recommended fuzzing proof +verification routines. + +### Mutation Testing + +Run mutation testing weekly or before release. + +For Solidity: + +```bash +slither-mutate src --test-cmd "forge test" --ignore-dirs "test,script,mocks" +``` + +Focus mutations on: + +| Mutation | Must be caught | +| --- | --- | +| Remove `input < q`. | Yes. | +| Remove `publicInputs.length` check. | Yes. | +| Replace `require(success)` after precompile with no-op. | Yes. | +| Remove subgroup check. | Yes. | +| Change VK selector key. | Yes. | +| Remove event emission for verifier upgrade. | Yes. | +| Comment out forced-exit timelock. | Yes. | + +For Rust: + +```bash +cargo mutants +cargo test +cargo llvm-cov --html +``` + +Require that mutants in verifier checks, transcript construction, range +checks, subgroup checks, and serialization are killed. + +High-value mutators for this repository: + +| Mutation | Should be caught by | +| --- | --- | +| Remove public-input `< r` check. | Canonicality tests. | +| Replace `< r` with `<= r`. | Boundary tests. | +| Remove point-on-curve check. | Invalid point tests. | +| Remove subgroup check. | Subgroup tests. | +| Ignore pairing output. | Corrupted proof tests. | +| Ignore `staticcall` success. | Precompile mock tests. | +| Skip one transcript absorption. | Transcript coverage tests. | +| Replace challenge with zero. | Challenge tests and invalid proof tests. | +| Remove proof length check. | Calldata tests. | +| Change one proof offset by 32 bytes. | Layout tests. | +| Remove `inv(0)` check. | Algebra tests. | +| Allow empty accumulator. | PCS tests. | +| Replace `&&` with `||` in validation. | Negative tests. | +| Remove optional component consistency check. | LogUp tests. | + +Trail of Bits describes mutation testing as changing target lines and rerunning +the test suite; surviving mutants indicate coverage gaps. The practical metric +is simple: if deleting a verifier-critical check does not break a test, that +test class is missing. + +### Lightweight Formal Methods + +Do not start by trying to formally verify the whole verifier. First verify the +small reusable helpers where one bug is catastrophic. + +#### Scribble, Foundry, Echidna, And Runtime Annotations + +Annotate verifier wrappers with executable invariants: + +```solidity +/// if_succeeds {:msg "public input length"} publicInputs.length + 1 == vk.icLen; +/// if_succeeds {:msg "field canonical"} forall(uint i in 0...publicInputs.length) publicInputs[i] < Q; +/// if_succeeds {:msg "vk shape"} vk.circuitId == expectedCircuitId; +/// if_succeeds {:msg "forced exit verifier not disabled"} forcedVerifier != address(0); +``` + +Best targets: + +| Contract area | Property | +| --- | --- | +| Public input builder | Solidity output equals reference output. | +| Verifier registry | VK identity includes full circuit shape/version. | +| Upgrade functions | Exits cannot be disabled without delay. | +| Deposit/epoch/withdraw flows | Only canonical encodings accepted. | +| Forced withdrawal | Eventually executable unless user cancels or proof is invalid. | + +#### Halmos Or SMTChecker + +Use symbolic execution for small bounded properties: + +```text +No accepted input has publicInputs[i] >= q. +No valid call reaches pairing precompile with malformed memory length. +No call to _verifyProof occurs unless publicInputs.length + 1 == vk.icLen. +Digest builder is injective for bounded request fields. +Withdrawal slot list is strictly sorted and unique. +``` + +This is lightweight because it does not prove SNARK soundness; it proves the +wrapper cannot violate its own validation rules. + +#### Lean Transcript Checker + +Model transcript structure, not the whole cryptography. + +For every verifier, extract: + +```text +domain separators +absorbed public inputs +absorbed commitments +challenge squeezes +MSM equations / final check terms +``` + +Then prove or check: + +```text +Every prover-controlled value used with challenge c was absorbed before c. +Every public input is absorbed before any challenge depending on the statement. +No challenge label is reused across incompatible proof contexts. +``` + +Trail of Bits used this style in the Token-2022 audit: an extractor produced +transcript traces, challenge labels, and MSM structures, then Lean checked +whether prover-controlled values appeared too late relative to challenge +derivation. + +#### TLA+, Alloy, Or Tamarin State Models + +Use protocol-state models for liveness and authorization, not proof-system +math. + +Model: + +```text +trees, treeNumber, knownRoots +auth keys, revocation, snapshots +nullifiers, pending forced withdrawals, spent nullifiers +verifier upgrades, timelocks +relay censorship +``` + +Check: + +```text +ForcedExitEventuallyPossible +NoSpentNullifierCanBeForcedWithdrawn +NoForcedExitCanBePermanentlyBlockedByRequesterFrontRun +VerifierUpgradeCannotImmediatelyDisableExit +RolloverNeverMakesAllProofsUnsatisfiable +``` + +Trail of Bits used Tamarin for state-machine properties in Token-2022, +including authorization and state-transition correctness. Symbolic models are +not fine-grained enough to prove ZK soundness, but they are useful for safety +and protocol-state properties. + +#### Helper-Level Formal Specs + +Good targets for SMT, Dafny, Why3, or Lean: + +| Helper | Property | +| --- | --- | +| `inv(x)` | If returns `y`, then `x != 0 && x*y == 1 mod r`. | +| `batch_invert(xs)` | Rejects any zero or returns valid inverses for all nonzero inputs. | +| `lagrange(i, zeta)` | Correct at `zeta` in domain and outside domain. | +| `pow_mod` wrapper | Result matches mathematical exponentiation or reverts on failed precompile. | +| `validate_scalar` | Accepts iff `0 <= x < r`. | +| `validate_g1/g2` | Accepts iff canonical, on curve, subgroup-valid, and infinity policy satisfied. | +| `parse_proof` | Exact proof length; every field read once; no overlap; no out-of-bounds. | +| `transcript_absorb` | Transcript is injectively encoded with length/domain tags. | +| `pairing_wrapper` | Returns true iff call succeeds, returndata is 32 bytes, and result word is 1. | +| `success flag` | Once false, never becomes true again. | + +Linea's Dafny appendix is a useful model: it did not prove the whole PLONK +verifier, but it verified targeted properties such as termination, +overflow/underflow, immutable inputs, state modification, and that +`state_success` could not be reset from false back to true. + +### Core Verifier Regression Suite + +For every generated verifier: + +| Test | Expected | +| --- | --- | +| Reference prover valid proof. | `verify == true`. | +| Reference verifier accepts but contract rejects. | Fail test. | +| Contract accepts but reference verifier rejects. | Critical failure. | +| Flip one bit in every proof scalar/point/evaluation. | Reject. | +| Swap two proof fields. | Reject. | +| Replace each commitment with another valid curve point. | Reject. | +| Use valid proof with wrong public input. | Reject. | +| Use valid proof with wrong VK/circuit ID/domain size. | Reject. | + +This catches pairing-result ignored bugs, skipped opening checks, and wrong +transcript/layout wiring. + +#### Calldata And Proof Layout Tests + +Run these against every entry point: + +| Mutation | Expected | +| --- | --- | +| Proof length `expected - 1`. | Revert or false. | +| Proof length `expected + 1`. | Revert or false. | +| Extra trailing bytes. | Reject unless explicitly allowed and transcript-bound. | +| Truncated public input array. | Reject. | +| Extra public inputs. | Reject. | +| Dynamic ABI overlap or malformed offsets. | Reject. | +| Zero public inputs when nonzero required. | Reject. | +| Zero or multiple custom gate commitments, if variants exist. | Supported or explicitly rejected. | + +This directly targets proof-size and calldata-overlap issues such as Linea's +missing proof length check. + +### Field, Scalar, And Curve Validation Tests + +#### Public Input Canonicality + +For each public input slot: + +| Value | Expected | +| --- | --- | +| `0`. | Accepted only if semantically valid. | +| `r - 1`. | Accepted only if semantically valid. | +| `r`. | Reject. | +| `r + 1`. | Reject. | +| `2^256 - 1`. | Reject. | +| `x + r` for a valid `x`. | Reject. | +| Non-canonical encoding of same semantic value. | Reject. | + +Linea's audit specifically recommends public inputs be checked `< r_mod`. + +#### Proof Scalar Canonicality + +For every scalar proof field: + +| Mutation | Expected | +| --- | --- | +| `s + r`. | Reject. | +| `r`. | Reject. | +| `2^256 - 1`. | Reject. | +| Zero where denominator/challenge must be nonzero. | Reject. | + +This catches scalar-multiplication malleability where ECMUL accepts a scalar +modulo the group order unless the verifier checks it itself. + +#### G1/G2 Point Validation + +For each proof/VK point: + +| Mutation | Expected | +| --- | --- | +| `(0,0)` / infinity encoding. | Reject unless explicitly allowed. | +| `x >= p`. | Reject. | +| `y >= p`. | Reject. | +| Valid field elements not on curve. | Reject. | +| Wrong subgroup point, if curve has cofactors. | Reject. | +| Swapped `x,y`. | Reject. | +| Valid point from another proof. | Reject. | +| Compressed point with invalid sign bit / non-residue `x`. | Reject. | + +Do not rely only on later precompile failure for these in wrappers. Linea's +audit recommended explicit field, group, and curve-point checks for proof +elements. + +### Precompile-Wrapper Tests + +Wrap ECADD, ECMUL/G1MSM, pairing, modexp, SHA/KZG/IPA/FRI helpers behind small +internal functions and test them in isolation with a test-only mock or +precompile adapter. + +| Mock behavior | Expected | +| --- | --- | +| `staticcall` fails. | Revert or false. | +| `staticcall` succeeds but returns no data. | Revert or false. | +| `staticcall` succeeds but returns 0 for pairing. | Reject. | +| `staticcall` succeeds and returns 1 for pairing. | Continue. | +| Returndata shorter than 32 bytes. | Reject. | +| Returndata longer than 32 bytes. | Reject or strictly decode first word by spec. | +| Stale memory prefilled with `1`, then failed call. | Reject. | +| Low gas to wrapper. | Reject without using stale return buffer. | + +This would catch missing pairing-result checks and stale-memory/staticcall +issues where checking only call success, or reusing an old output buffer, lets +invalid proofs pass. + +### Algebra Edge-Case Tests + +#### Inversion And Batch Inversion + +| Input | Expected | +| --- | --- | +| `inv(0)`. | Revert. | +| `inv(x) * x mod r == 1` for random nonzero `x`. | Pass. | +| Batch inversion with all nonzero elements. | Each `a[i] * inv[i] == 1`. | +| Batch inversion with one zero at every position. | Revert or documented zero-handling. | +| Empty array. | Reject unless explicitly supported. | + +Linea found both "inverse of zero returns zero" and batch inversion failures +when an element becomes zero. + +#### Roots Of Unity And Lagrange Evaluation + +For domain `H = {omega^i}`: + +| Test | Expected | +| --- | --- | +| `L_i(omega^i)`. | `1`. | +| `L_i(omega^j), i != j`. | `0`. | +| `zeta = 1`. | Special-case handled. | +| `zeta = omega^i` for every `i`. | Special-case handled. | +| Random `zeta` not in `H`. | Matches reference implementation. | +| `Z_H(zeta) == 0`. | No division by zero. | + +This catches root-of-unity Lagrange bugs where an efficient formula returns an +incorrect value at domain points. + +### Fiat-Shamir Transcript Tests + +Create an independent transcript oracle in Rust, Go, or Python and generate +fixed test vectors. + +For each transcript round: + +| Test | Expected | +| --- | --- | +| Changing any public input changes all later challenges. | Yes. | +| Changing any proof commitment changes later challenges. | Yes. | +| Changing any VK/SRS/domain/circuit digest changes challenges. | Yes. | +| Changing chain ID / verifier address / app domain changes challenges. | Yes. | +| Reordering fields changes challenges. | Yes. | +| Omitting optional component length/tag changes challenges. | Yes. | +| Challenge sampling never uses modulo reduction with unacceptable bias. | Rejection sampling or proof of acceptable bias. | + +Scroll flagged omitted curve parameters from Fiat-Shamir and recommends +including all public parameters in the transform. Stwo-Cairo had a +high-severity proof-forgery issue because public memory ID values were not +mixed into the Fiat-Shamir channel before interaction challenges were drawn. + +A powerful mutation test is: delete one transcript absorption line and require +at least one test to fail. If no test fails, that field was not covered by the +transcript tests. + +### PCS, Pairing, FRI, And Merkle Proof Tests + +For SNARK/KZG/IPA: + +| Mutation | Expected | +| --- | --- | +| Replace `Wz`. | Reject. | +| Replace `Wzomega`. | Reject. | +| Replace one claimed evaluation. | Reject. | +| Replace one batched commitment. | Reject. | +| Empty accumulator vector. | Reject. | +| Duplicate accumulator. | Reject unless intended. | +| Remove one opening from batch. | Reject. | + +For STARK/FRI/Merkle: + +| Mutation | Expected | +| --- | --- | +| Shorten queried values array. | Reject before loop. | +| Add extra queried values. | Reject. | +| Wrong Merkle sibling. | Reject. | +| Wrong query position. | Reject. | +| Wrong FRI folded value. | Reject. | +| Empty FRI layer vector. | Reject. | +| Mismatched number of decommitments. | Reject. | +| Wrong preprocessed trace/root. | Reject. | + +Stwo-Cairo's audit explicitly recommends validating prover-supplied data +before it shapes verifier execution, including `zip` length checks. A shorter +prover array must never skip Merkle verification. + +### Recursive Verifier And Circuit-Specific Adversarial Tests + +For Halo2, Circom, and STARK recursive verifiers, happy-path proofs are not +enough. Add malicious witness tests. + +#### Constraint Soundness Tests + +For each gadget, build a valid witness, then mutate one witness-only value: + +| Gadget class | Mutations | +| --- | --- | +| Boolean flags | Set to `2`, `-1`, or a random field element. | +| Range flags | Set `is_lt=false` for small value, `is_lt=true` for large value. | +| Lookup selector | Set unused or extra component. | +| Memory ID / table ID | Alter ID after transcript challenge. | +| Error/success selector | Make both success and error paths satisfiable. | +| Opcode selector | Mismatch opcode and execution state. | +| Gas/counter witness | Undercount or overcount by 1. | +| Public IO aggregate | Compute but do not expose/constrain. | + +Scroll's findings show why: several high-impact bugs came from +underconstrained witness values, nondeterministic execution, and success/error +state overlap. + +#### Determinacy Tests + +For each gadget or opcode: + +```text +Given the same public inputs and pre-state, there must not exist two +satisfying witnesses with different post-state/output. +``` + +Implementation options: + +1. For small gadgets, brute-force all inputs over a small field/model. +2. For medium gadgets, use SMT over bounded integers. +3. For full circuits, run mutated-witness negative tests with + `MockProver`/constraint checker. + +Trail of Bits specifically recommended determinacy testing for gadgets that +constrain nondeterministic witnesses. + +### Static Lints And Semgrep Rules + +Add rules that fail CI on dangerous verifier patterns: + +| Rule | Pattern | +| --- | --- | +| Unchecked precompile. | `pop(staticcall(...))`. | +| Pairing result ignored. | Pairing precompile call without checking returned word is 1. | +| Stale return buffer. | Output buffer reused without zeroing/checking returndata size. | +| Unsafe modulo inverse. | `pow(x, p-2)` without prior `x != 0`. | +| Public input reduction. | `input % r` instead of `require(input < r)`. | +| Scalar multiplication without scalar range check. | ECMUL/G1MSM called with unconstrained scalar. | +| Proof-driven `zip`. | `zip(proof_array)` without length equality assertion. | +| Optional component sum. | `Option::Some(sum)` allowed when component claim is `None`. | +| Zero-padding hash. | Hash pads without length/tag/domain separator. | +| Debug-only invariant. | `debug_assert` or release-disabled `assert` for critical checks. | +| TODO/FIXME in verifier path. | No unresolved security-relevant TODOs. | + +Scroll used Semgrep to search for variants after identifying vulnerable +patterns, and its automated testing focused on dangerous Halo2-specific/API +patterns. + +### CI Release Gate + +Use this schedule: + +```text +Every PR: + forge test + forge test --fuzz-runs 10000 + native unit tests + public-input differential tests + negative proof fixtures + static analysis: slither, semgrep, clippy, cargo-audit + +Nightly: + forge invariant --runs high + echidna/medusa campaign + cargo fuzz / go fuzz proof parsers + mutation testing subset + long-horizon rollover simulation + +Pre-release: + full mutation testing + native-vs-EVM verifier differential corpus + Lean transcript extraction/check + TLA+/Alloy/Tamarin state-machine checks + regenerated verifier bytecode hash check +``` + +Minimum release blockers: + +1. 100% of proof fields have at least one corruption test. +2. 100% of public inputs have canonicality tests. +3. Every precompile wrapper has failure/stale-memory tests. +4. Every transcript field has an omission mutation that fails tests. +5. Every proof-provided array/option has length/consistency tests. +6. Every generated offset is checked against a single layout manifest. +7. Every helper with division/inversion has zero tests and a small formal spec. + +### Bug-To-Test Mapping + +| Bug class | Best detector | +| --- | --- | +| Missing public input length check. | Unit and mutation test. | +| Non-canonical field input. | Fuzz and metamorphic test. | +| Wrong VK/circuit shape. | Differential and registry invariant. | +| ABI malleability. | Calldata fuzz and canonical decode/encode property. | +| Missing subgroup/on-curve check. | EC negative fixtures. | +| Transcript omission. | Lean transcript checker. | +| Empty accumulator accepted. | Negative proof test. | +| Underconstrained equality/range gadget. | Circuit negative tests. | +| Tree rollover liveness failure. | Long-horizon invariant fuzz. | +| Forced withdrawal can be blocked. | TLA+/Alloy state model and Foundry invariant. | +| Merkle proof forgery. | Metamorphic inclusion/non-inclusion tests. | +| Panic on malformed proof. | Fuzz proof parser/verifier. | +| Removed validation not caught by tests. | Mutation testing. | + +The highest ROI is negative tests, differential tests, mutation testing, and +one lightweight transcript/state model. That combination catches a large +fraction of verifier failures in the audit corpus without requiring full +formal verification of the proof system. + +--- + +## Quotient Evaluation Compiler-Correctness Strategy + +Treat quotient evaluation as a **compiler-correctness problem**, not only as a +verifier test problem. The core invariant is: + +```text +Rust source identity stream + == typed quotient IR + == optimized quotient IR + == VM bytecode / native callbacks / direct Yul + == EVM execution trace +``` + +Final `verify(proof) == true/false` tests are necessary, but they are too +coarse. They can miss wrong-`y`-power, wrong-selector-bucket, and +native-callback-order bugs. The tests should compare **internal linearization +artifacts**: identity values, fold targets, selector buckets, main numerator, +final `QUOTIENT_EVAL_MPTR`, and commitment-side coefficients. + +This matches the assurance style recommended in adjacent Halo2/zkEVM audits: +adversarial testing, explicit specifications, and using Rust's type system to +enforce invariants rather than relying on conventions. High-complexity circuit +logic also needs unit tests, negative tests, and documentation wherever +soundness depends on subtle invariants. + +### 1. Standalone quotient-evaluator oracle + +Extract the quotient evaluator into a testable function that does **not** +require a valid proof. Give it a synthetic verifier memory frame: + +```rust +struct QuotientFrame { + challenges: Challenges, // x, y, beta, gamma, theta, trash challenge, ... + evals: HashMap, // advice/fixed/instance/permutation/lookup evals + vk_constants: Vec, + domain: DomainParams, +} +``` + +Then define several evaluators over the same frame: + +```rust +fn eval_upstream_midnight(frame) -> LinearizationArtifacts; +fn eval_typed_ir(frame) -> LinearizationArtifacts; +fn eval_vm_interpreter(frame, q_program) -> LinearizationArtifacts; +fn eval_yul_reference(frame, generated_solidity) -> LinearizationArtifacts; +fn eval_evm_debug_contract(frame) -> LinearizationArtifacts; +``` + +Where: + +```rust +struct LinearizationArtifacts { + identity_trace: Vec, + main_numerator: Fr, + selector_buckets: Vec, + quotient_expected_eval: Fr, // -main_numerator +} + +struct IdentityTraceEntry { + global_index: usize, + source: IdentitySource, + target: IdentityTarget, + value: Fr, + y_power: usize, +} +``` + +The most valuable property is: + +```text +for random frame: + upstream_midnight == typed_ir + typed_ir == vm_interpreter + vm_interpreter == evm_debug_contract +``` + +This avoids relying on valid proofs. Randomize every claimed polynomial +evaluation and every challenge, then check pure algebraic equivalence. + +### 2. Halo2 gate expression == generated QuotientExpr / Yul / VM bytecode + +For every gate polynomial, test four layers. + +First, compare Halo2 expression evaluation against `QuotientExpr` evaluation: + +```rust +proptest! { + #[test] + fn halo2_expression_matches_quotient_expr( + expr in generated_halo2_exprs(), + frame in random_frame(), + ) { + let qexpr = quotient_expr_from_expression(&env, &expr); + assert_eq!( + eval_halo2_expression(&expr, &frame), + eval_quotient_expr(&qexpr, &frame) + ); + } +} +``` + +Include all expression variants used by the circuit: + +```text +Constant +Fixed / Advice / Instance at rotations -1, 0, +1, +k +Challenge +Negated +Sum +Product +Scaled +``` + +Second, compare `QuotientExpr` to VM bytecode: + +```rust +for identity in all_gate_identities { + let qexpr_value = eval_quotient_expr(identity.expr, frame); + let vm_value = eval_isolated_identity_vm(identity.vm_program_slice, frame); + assert_eq!(qexpr_value, vm_value); +} +``` + +Third, compare VM bytecode to actual EVM execution. Add a debug-only generated +entrypoint: + +```solidity +function debugQuotient(bytes calldata encodedFrame) + external + returns ( + uint256 mainNumerator, + uint256 quotientExpectedEval, + uint256[] memory selectorBuckets, + uint256[] memory identityTrace + ); +``` + +This function should run the same Yul quotient block but return trace data +before the PCS check. + +Fourth, run the same circuit under all codegen modes: + +```text +direct Yul +VM bytes +VM packed32 +VM packed256 +VM CSE on/off +inline CSE on/off +pow5 helper on/off +limb opcodes on/off +structured tail on/off +native gates 0 / 1 / many +``` + +For a fixed random frame, every mode must produce exactly the same +`LinearizationArtifacts`. + +### 3. Optimizer peephole tests + +The dangerous part of this codegen is not `addmod`; it is semantic compression. +For each peephole, create a generic reference expression and compare it against +the special opcode. + +#### POW5 + +Test all equivalent product-tree shapes: + +```text +((((a*a)*a)*a)*a) +(a*a)*(a*a)*a +a*(a*(a*(a*a))) +``` + +Reject near-misses: + +```text +a^4 * b +a^5 + c +(-a)^5 if recognizer is not designed for it +``` + +#### LIN7 + +Reference: + +```text +sum_i coeff_i * limb_i +``` + +Test: + +```text +random coeffs +zero coeffs +duplicate coeffs +limb memory pointers not contiguous +constant-slot boundary at 254, 255, 256 +``` + +#### BILIN7_ROW + +Reference: + +```text +lhs * sum_i coeff_i * rhs_i +``` + +Test both multiplication orderings: + +```text +lhs * rhs_i +rhs_i * lhs +``` + +and reject non-row shapes where two different lhs values appear. + +#### BILIN7_PAIRWISE + +Reference: + +```text +sum_{i=0..6} sum_{j=0..6} coeff_{i+j} * lhs_i * rhs_j +``` + +Test: + +```text +contiguous limb vectors +swapped lhs/rhs vectors +missing one of 49 products +wrong coeff for one i+j diagonal +duplicate product that cancels another product +nonzero residue outside pairwise shape +``` + +#### MODARITH7 + +Reference: + +```text +cond? * ( + constant + + sum LIN7 + + sum BILIN7_ROW + + sum BILIN7_PAIRWISE + + sum coeff_i * mem_i + + sum coeff_i * lhs_i * rhs_i +) +``` + +Test with: + +```text +cond absent / present +constant absent / present +only sparse products +dense limb blocks plus sparse residue +zero cond +cond = 1 +cond = random Fr +all count fields at 0, 1, multiple +``` + +The test should fail if `MODARITH7` mutates the constant table before deciding +the shape cannot be emitted. + +### 4. Rust identity order == Solidity VM/native callback order + +Generate an **identity manifest** before any lowering: + +```json +[ + { + "global_index": 0, + "source": "Gate", + "gate_index": 0, + "constraint_index": 0, + "target": "Main" + }, + { + "global_index": 1, + "source": "Gate", + "gate_index": 1, + "constraint_index": 0, + "target": { "Selector": 0 } + }, + { + "global_index": 2, + "source": "Permutation", + "kind": "FirstBoundary", + "set": 0, + "target": "Main" + } +] +``` + +Then assert: + +```text +manifest from upstream Rust + == manifest from quotient planner + == manifest reconstructed from q_program fold opcodes and native markers + == manifest emitted by debug EVM trace +``` + +For native callbacks, do not merely assert "callback opcode exists." Assert: + +```text +native_permutation marker expands to identities [i, i+1, ..., j] +native_lookup marker expands to identities [k, ..., l] +native_identity(n) expands to exactly identity m +``` + +Add a test that randomly toggles native lowering: + +```text +same circuit, same frame: + all interpreted + native permutation only + native lookup only + native gate 0 only + all native callbacks enabled +must produce identical identity trace and final artifacts +``` + +A native callback should be modeled as a **range replacement** in the manifest, +not as arbitrary Yul text. + +### 5. Simple-selector classification and bucket-target tests + +Create small synthetic circuits where selector behavior is obvious. + +#### One simple selector, one identity + +```text +e_0 = q_simple * body +``` + +Expected: + +```text +main_numerator = 0 +selector_bucket[q_simple] = body +``` + +after the appropriate tail power. + +#### One simple selector, multiple identities + +If identities are at global positions: + +```text +i = 1, 4, 7 +m = 10 +``` + +then the selector bucket must equal: + +```text +body_1 * y^(m-1-1) ++ body_4 * y^(m-1-4) ++ body_7 * y^(m-1-7) +``` + +Test this directly, not just through Horner folding. + +#### Multiple simple selectors interleaved + +Example identity stream: + +```text +0 main +1 selector A +2 selector B +3 main +4 selector A +5 selector B +6 selector A +``` + +Expected: + +```text +A = e_1*y^5 + e_4*y^2 + e_6 +B = e_2*y^4 + e_5*y +main = e_0*y^6 + e_3*y^3 +``` + +This catches off-by-one errors in selector gaps and tails. + +#### Misclassification tests + +Create gates with: + +```text +no selector +one simple selector +two queried selectors, one simple +complex selector expression +simple selector converted to fixed column +ordinary fixed column that is not a simple selector +``` + +Assert the exact `QuotientTarget`: + +```text +simple selector => Selector(index) +ordinary fixed selector => Main +complex selector expression => Main +``` + +The target decision should be tested before and after selector-to-fixed +conversion. + +### 6. Permutation chunking and delta-power tests + +Permutation is high-risk because wrong chunk offsets can still look +algebraically plausible. For each generated permutation identity, compare +against an independent reference implementation. + +Test configurations: + +```text +chunk_len = 1 +chunk_len = 2 +chunk_len = degree - 1 +number of permutation columns not divisible by chunk_len +one permutation z +multiple permutation z polynomials +advice-only columns +fixed columns +instance columns +mixed column types +rotations used in permutation evals +``` + +For each chunk `set_idx`, assert the exact exponent sequence: + +```text +initial_delta_power = delta^(set_idx * chunk_len) +delta_pow_0 = beta * x * delta^(set_idx * chunk_len) +delta_pow_j = beta * x * delta^(set_idx * chunk_len + j) +``` + +Add explicit mutation tests that must fail: + +```text +delta^(set_idx + chunk_len) instead of delta^(set_idx * chunk_len) +starting delta power at j=1 instead of j=0 +using z_cur where z_next is required +using permutation eval on right side instead of left side +omitting gamma from one product factor +omitting active_rows = 1 - Llast - Lblind +using L0 instead of active_rows +``` + +For EVM debug runs, expose each permutation identity trace entry: + +```text +Permutation::FirstBoundary(set) +Permutation::LastBoundary(set) +Permutation::Continuity(set) +Permutation::Product(set, chunk_start, chunk_len) +``` + +and compare with the Rust manifest. + +### 7. Lookup chunking, theta-compression, and helper-product tests + +Lookup tests should cover both algebra and chunk scheduling. + +#### Theta-compression + +For input expressions: + +```text +[e0, e1, e2] +``` + +assert: + +```text +compress = ((0 * theta + e0) * theta + e1) * theta + e2 +``` + +Add a mutation test for reversed compression order: + +```text +e0 + theta*e1 + theta^2*e2 +``` + +depending on the intended convention. The test should make the convention +unambiguous. + +#### Helper identity + +For `k` parallel inputs: + +```text +f_j = compressed_j + beta +P = product f_j +sum = sum_j product_{t != j} f_t +identity = h * P - sum +``` + +Test: + +```text +k = 1 +k = 2 +k = max allowed by degree chunking +one f_j = 0 +two equal f_j values +random f_j values +``` + +For `k = 1`, expected: + +```text +P = f0 +sum = 1 +h * f0 - 1 +``` + +This catches prefix/suffix off-by-one bugs. + +#### Accumulator identity + +Assert exactly: + +```text +active * ( + (z_next - z - selector * sum_h) + * (compressed_table + beta) + + m +) +``` + +Mutation tests: + +```text +z - z_next instead of z_next - z +selector omitted +sum_h replaced by last h +m subtracted instead of added +table compressed with beta instead of theta +input compressed with trash challenge instead of theta +active rows omitted +``` + +#### Shared-prefix optimization + +The `lookup_shared_prefix_f_plus_beta` optimization deserves its own tests: + +```text +shared prefix with adjacent tails => optimized path +same prefix but non-adjacent tails => fallback path +same tail layout but different prefix => fallback path +single input => fallback path +empty prefix => fallback path +``` + +The optimized and fallback paths must produce identical `f_plus_beta` arrays. + +### 8. Trash identity tests + +Trash is part of the same y-batched stream and can break the final scalar. + +Test: + +```text +zero trash constraint expressions +one trash constraint expression +multiple expressions +selector = 0 +selector = 1 +selector = random Fr +trash_eval = 0 +trash_eval = random Fr +``` + +Assert: + +```text +compressed = tau-fold(expressions) +identity = compressed - (1 - selector) * trash_eval +``` + +Mutation tests: + +```text +using theta instead of tau +using selector * trash instead of (1 - selector) * trash +compressing in reverse order +failing to include trash identities in y-batch order +``` + +### 9. VM encoding and bytecode-safety tests + +The Rust validator already checks stack safety. Expand it into fuzz tests. + +#### Valid-program fuzzing + +Generate random `QuotientExpr` trees: + +```text +depth 0..8 +constants +literal memory +token memory +token+offset memory +add/mul/neg +repeated subexpressions +``` + +Lower them to: + +```text +bytes +packed32 +packed256 +``` + +Then compare: + +```text +eval_expr == eval_bytecode == eval_packed32 == eval_packed256 +``` + +where supported. + +#### Invalid-program fuzzing + +Generate malformed bytecode and assert validation rejects: + +```text +unknown opcode +reserved 0x1a +truncated operand +invalid memory token +stack underflow +stack leak at end +fold with empty stack +fold with more than one stack value +native callback with non-empty stack +packed32 operand too wide +packed256 nonzero unused operand +dynamic run count = 0 +AFFINE_SUM with one side empty +MODARITH7 unknown flag bit +``` + +#### Runtime fail-closed tests + +Deploy generated Yul with a deliberately corrupted VK payload and assert it +reverts, not silently computes. + +Corrupt: + +```text +opcode byte +memory token +native identity index +packed instruction length +constant table offset +selector index +selector gap +``` + +### 10. Memory layout and aliasing tests + +The VM assumes memory is planned correctly. Add tests that inspect the generated +memory map. + +For every generated verifier, assert regions are disjoint: + +```text +proof eval memory +challenge memory +quotient constant table +q_program bytes +q_stack +q_tmp +selector accumulator buckets +selector power table +native permutation scratch +native lookup scratch +post-VM scratch +``` + +For native callbacks, assert: + +```text +allocated stack/scratch words + >= max( + pure VM max stack, + structured_permutation_scratch_words(meta), + structured_lookup_scratch_words(meta), + native identity scratch + ) +``` + +Then add a canary test in debug EVM mode: + +```text +fill all adjacent memory regions with random canaries +run quotient evaluator +assert canaries outside declared scratch regions unchanged +``` + +This is especially important because the VM stack pointer is reused as native +callback scratch. + +### 11. End-to-end proof tests + +Once the algebraic evaluator tests pass, add actual verifier tests. + +For each circuit fixture: + +```text +native Rust verifier accepts valid proof +Solidity verifier accepts valid proof +native Rust verifier rejects mutated proof +Solidity verifier rejects same mutated proof +``` + +Mutate one thing at a time: + +```text +one advice eval +one fixed eval +one instance eval +one quotient commitment +one permutation z commitment +one lookup h/z/m eval +one challenge transcript input +one public input +one proof opening scalar +one G1 proof point +one q_eval point-set scalar +``` + +Also mutate structured values: + +```text +swap two evals of same type +swap current and next rotation +swap two lookup chunks +swap two permutation chunks +change one simple selector eval if present +``` + +Do not stop at final `false`. In debug mode, assert which identity trace +diverges. + +### 12. Mutation testing + +Add permanent "compiler mutant" tests. These should fail if any of the +following changes are introduced: + +```text +final expected eval = +nu instead of -nu +fold = acc + y*eval instead of acc*y + eval +main fold does not advance y for selector identity +selector gap is off by one +selector tail is off by one +selector bucket uses global gap instead of per-selector gap +native callback emits one fewer fold +native callback emits one extra fold +permutation left/right swapped +permutation delta exponent starts from wrong set offset +lookup theta-compression reversed +lookup helper sum excludes wrong index +lookup accumulator subtracts m instead of adding m +trash uses theta instead of tau +simple selector fixed query is loaded from memory instead of replaced with 1 +``` + +Mutation testing is a good fit here because many codegen mistakes are +single-token changes that ordinary positive tests may not kill. + +### 13. Correct-by-construction rearchitecture + +The current architecture has several duplicated semantic surfaces: + +```text +Halo2 Expression evaluator +QuotientExpr lowering +legacy Yul string emitter +VM bytecode emitter +native callback Yul emitter +Yul interpreter template +Rust bytecode validator +opcode documentation table +``` + +The goal should be to collapse these into one typed semantic pipeline. + +Recommended pipeline: + +```text +ConstraintSystem + -> IdentityStream + -> Normalized QuotientExpr DAG + -> Semantics-preserving lowering passes + -> Executable Quotient Program IR + -> Physical encoding: bytes / packed32 / packed256 / direct Yul + -> Generated Solidity +``` + +Each stage should have an `eval(frame) -> LinearizationArtifacts` method. + +#### 13.1 Make `IdentityStream` the single source of truth + +Define: + +```rust +struct IdentityStream { + identities: Vec, + simple_selectors: Vec, +} + +struct Identity { + id: IdentityId, + global_index: usize, + source: IdentitySource, + target: IdentityTarget, + expr: QuotientExpr, +} + +enum IdentitySource { + Gate { + gate_index: usize, + gate_name: String, + constraint_index: usize, + constraint_name: String, + }, + Permutation(PermutationIdentityKind), + Lookup(LookupIdentityKind), + Trash(TrashIdentityKind), +} + +enum IdentityTarget { + Main, + Selector { selector_index: usize, fixed_column: usize }, +} +``` + +Everything downstream consumes this. No downstream code should independently +decide identity order, selector target, or fold count. + +#### 13.2 Stop parsing generated Yul back into expressions + +Parsing Yul assignments into `QuotientExpr` is inherently fragile. Instead: + +```text +Halo2 Expression + -> QuotientExpr + -> direct Yul + -> VM bytecode + -> native callback +``` + +The Yul should be a printer for typed IR, not an intermediate representation. +If some identities still need special manual generation, model them as typed +nodes: + +```rust +enum QuotientExpr { + Const(Fr), + Mem(MemRef), + Add(...), + Mul(...), + Neg(...), + Compress { challenge: Challenge, terms: Vec }, + Product(Vec), + PermutationProduct(PermutationProductSpec), + LookupHelper(LookupHelperSpec), +} +``` + +Then lower the typed node to either generic arithmetic or native loops. + +#### 13.3 Generate opcode semantics and Yul switch cases from one table + +The opcode table, bytecode validator, packed encoders, and Yul switch cases +must stay synchronized. Define one Rust macro/table: + +```rust +quotient_opcode! { + PushConst { + opcode: 0x01, + stack: [] -> [Fr], + operands: [ConstSlotU16], + encodings: [Bytes, Packed32, Packed256], + semantics: |state, slot| state.push(state.consts[slot]), + yul: yul_push_const, + } + + FoldMain { + opcode: 0x0a, + stack: [Fr] -> [], + operands: [], + boundary: IdentityBoundary, + semantics: |state, eval| state.fold_main(eval), + yul: yul_fold_main, + } +} +``` + +Generate from this table: + +```text +opcode constants +Rust decoder +Rust encoder +Rust validator +Rust interpreter +Yul switch arms +documentation table +tests for operand widths +``` + +That makes opcode drift much harder. + +#### 13.4 Make native callbacks range replacements, not arbitrary insertions + +Represent native callbacks as: + +```rust +enum ProgramItem { + InterpretedIdentity(IdentityId), + NativeRange { + kind: NativeKind, + identity_ids: Range, + expected_targets: Vec, + }, +} +``` + +Then require: + +```rust +fn lower_native_range(range) -> NativeCallback { + assert_eq!(callback.fold_manifest(), range.identity_manifest()); +} +``` + +A callback should return a compile-time/test-time certificate: + +```rust +struct CallbackCertificate { + identity_ids: Vec, + targets: Vec, + fold_count: usize, +} +``` + +The planner should reject a callback if its certificate does not exactly match +the original identity range. + +#### 13.5 Derive selector gaps and tails only from the manifest + +Selector folding should be a pure function: + +```rust +fn selector_fold_plan( + identities: &[Identity], + num_selectors: usize, +) -> SelectorFoldPlan; +``` + +Then test it exhaustively for small streams: + +```rust +for all target sequences of length <= 8: + sparse_selector_fold == naive_global_y_fold +``` + +The sparse algorithm should never manually track positions in emitter code. It +should consume: + +```text +global_index +target +total_identity_count +``` + +and output: + +```text +gap_by_identity +tail_by_selector +max_power +``` + +#### 13.6 Use typed memory references + +Avoid carrying raw `u16`, `u32`, and string pointer expressions until final +encoding. + +Use: + +```rust +struct MemRef { + region: RegionId, + offset_words: usize, + ty: PhantomData, +} + +enum RegionKind { + Challenge, + ProofEval, + FixedEval, + AdviceEval, + InstanceEval, + QuotientConstTable, + VmStack, + VmTemp, + SelectorAcc, + SelectorPower, + NativeScratch, +} +``` + +The memory planner should produce a proof object: + +```rust +struct MemoryLayoutProof { + regions: Vec, +} + +impl MemoryLayoutProof { + fn assert_non_overlapping(&self); + fn assert_aligned(&self); + fn assert_region_capacity(&self, region, words); +} +``` + +Only the final encoder converts `MemRef` into a literal pointer or +token-offset pair. + +#### 13.7 Use typestates for transcript phase and challenge availability + +The verifier flow depends on when challenges are sampled. Encode this: + +```rust +struct Builder { ... } + +struct BeforeX; +struct AfterX; +struct AfterEvals; +struct AfterY; + +impl Builder { + fn read_quotient_commitments(self) -> Builder; +} + +impl Builder { + fn squeeze_x(self) -> Builder; +} + +impl Builder { + fn read_evaluations(self) -> Builder; +} + +impl Builder { + fn build_quotient_numerator(self) -> Builder; +} +``` + +This prevents accidental use of an evaluation or challenge before the +transcript has made it available. + +#### 13.8 Make permutation and lookup identities explicit typed specs + +Instead of hand-emitting formula fragments, define: + +```rust +enum PermutationIdentityKind { + FirstBoundary { set: usize }, + LastBoundary { set: usize }, + Continuity { set: usize, previous_set: usize }, + Product { + set: usize, + chunk_start: usize, + columns: Vec, + delta_start_exp: usize, + }, +} + +enum LookupIdentityKind { + Boundary { lookup: usize }, + Helper { lookup: usize, chunk: usize, arity: usize }, + Accumulator { lookup: usize, num_chunks: usize }, +} +``` + +Each kind implements: + +```rust +fn to_expr(&self, env: &Env) -> QuotientExpr; +fn eval_reference(&self, frame: &Frame) -> Fr; +``` + +Then test: + +```text +to_expr().eval(frame) == eval_reference(frame) +``` + +This makes permutation chunking and lookup chunking auditable as data, not as +generated code. + +#### 13.9 Require every optimization pass to be semantics-preserving + +Each pass should have the shape: + +```rust +trait RewritePass { + fn rewrite(&self, input: ProgramIR) -> ProgramIR; + fn check_local_equivalence(&self, before: &Node, after: &Node) -> RewriteCertificate; +} +``` + +For example: + +```text +Pow5Rewrite: + before = Mul(a, Mul(a, Mul(a, Mul(a, a)))) + after = Pow5(a) + certificate = same base key appears exactly five times + +SelectorFactoring: + before = q_simple(x) * body(x) + after = target Selector(q_simple), expr body(x) + certificate = q_simple is in simple_selector_cols +``` + +For limb rewrites, include a debug-only symbolic expansion check: + +```rust +assert_eq!( + eval_generic_limb_expr(frame), + eval_limb_opcode_expr(frame) +); +``` + +Run that assertion over randomized frames in tests. + +#### 13.10 Hash the codegen ABI into the VK artifact + +The quotient VM is an ABI between the generated VK payload and generated +Solidity. Include a version/hash over: + +```text +opcode table +memory token table +physical encoding +q_program bytes +constant table +identity manifest hash +selector fold plan hash +native callback manifest hash +memory layout hash +``` + +A verifier artifact should fail generation or deployment checks if the template +opcode table and Rust opcode table disagree. + +### 14. CI strategy + +A good CI matrix would be: + +```text +cargo test +cargo test --features quotient_debug_trace +proptest quotient_expr_lowering +proptest quotient_vm_bytecode +proptest selector_fold_plan +proptest permutation_identities +proptest lookup_identities +foundry test debugQuotient +foundry test valid/invalid proof fixtures +mutation test selected quotient files nightly +gas snapshot for representative verifiers +``` + +Run the full cross-product nightly, not on every PR: + +```text +encoding: bytes / packed32 / packed256 +vm_cse: on / off +inline_cse: on / off +native_permutation: on / off +native_lookup: on / off +native_gates: 0 / 1 / many +limb_ops: on / off +structured_tail: off / trash +``` + +On every PR, run a smaller deterministic matrix: + +```text +direct Yul +VM bytes +VM packed32 +native callbacks all off +native callbacks all on +selector-heavy fixture +permutation-heavy fixture +lookup-heavy fixture +foreign-field-heavy fixture +``` + +### 15. Minimum high-value test suite + +If time is limited, prioritize these five: + +1. **Standalone quotient differential test:** `upstream Rust == typed IR == VM + interpreter == EVM debug contract` over random frames. +2. **Identity manifest test:** exact equality of identity order, source + metadata, target, and native callback fold ranges. +3. **Selector sparse-fold exhaustive test:** for all small target sequences, + compare sparse selector gap/tail folding to naive global y-power folding. +4. **Permutation/lookup typed-reference tests:** each generated permutation and + lookup identity must equal an independent formula evaluator. +5. **Mutation tests for signs, y powers, selector gaps, delta powers, theta + compression, and callback fold counts.** + +Those five directly target these failure modes: + +```text +Halo2 gate expression == generated QuotientExpr / Yul / VM bytecode +Rust identity order == Solidity VM/native callback order +simple selector classification == selector bucket target +permutation chunking / delta == upstream permutation argument +lookup chunking / theta/helper == upstream LogUp argument +native callbacks == same identities, same order, same y folds +``` + +The rearchitecture goal is to make those equalities not just tested, but +structurally hard to violate: one identity stream, one typed expression IR, one +opcode spec, one memory model, one selector schedule function, and native +callbacks represented as certified range replacements rather than hand-written +alternative semantics. diff --git a/proofs/solidity-verifier/docs/plans/quotient_eval_optim.md b/proofs/solidity-verifier/docs/plans/quotient_eval_optim.md new file mode 100644 index 000000000..fb7207853 --- /dev/null +++ b/proofs/solidity-verifier/docs/plans/quotient_eval_optim.md @@ -0,0 +1,475 @@ +# Quotient Evaluation Contract Size Optimization + +## Context + +The generated Solidity verifier currently emits the batched identity numerator +reconstruction as straight-line Yul/Solidity arithmetic. Historically we called +this "quotient evaluation", but the verifier is not directly evaluating +`h(x)`. It reconstructs `nu_y(x)` from alleged polynomial evaluations and stores +`-nu_y(x)` as the expected opening scalar for the linearized commitment. For +large Halo2/Midnight-ZK circuits this can produce thousands of arithmetic +statements. The runtime cost is pure `Fr` arithmetic, but the generated code +occupies a large amount of verifier bytecode. + +This matters because the main verifier contract must stay under the EIP-170 +runtime bytecode limit of 24 KiB when it is deployed on a normal EVM chain. Even +when the verifier is split from the verifying-key contract, the quotient evaluation +section can dominate the verifier bytecode. + +## Goal + +Reduce verifier bytecode size by replacing straight-line quotient identity code +with a compact interpreter: + +- Store the quotient identity computation as a dense bytecode-like program. +- Emit a small Yul interpreter loop once. +- Execute the program at verification time to reconstruct/evaluate the quotient + scalar. + +This trades higher verification gas for much smaller generated contract bytecode. +It is likely the best single-contract size reduction path for the quotient +evaluation section. + +## Current Shape + +The current generator expands quotient evaluation into direct arithmetic: + +- Load challenges, evaluations, constants, and powers. +- Multiply and add terms for each identity contribution. +- Accumulate the quotient numerator. +- Divide by the vanishing polynomial term or apply the equivalent reconstructed + quotient evaluation logic. + +The compiler sees all of this as unique code. Even if many identities share the +same pattern, the emitted bytecode still grows with the number of terms. + +## Proposed Shape: Compact Identity Interpreter + +Instead of emitting each identity as Yul statements, generate a compact program +plus a tiny interpreter. + +Example opcode vocabulary: + +| Opcode | Meaning | +| --- | --- | +| `LOAD_EVAL idx` | Push advice/fixed/instance evaluation `idx` | +| `LOAD_CHALLENGE idx` | Push transcript challenge `idx` | +| `LOAD_CONST idx` | Push constant field element `idx` | +| `LOAD_ACC idx` | Push accumulator/register `idx` | +| `ADD` | Pop two field elements, push `addmod(a, b, r)` | +| `SUB` | Pop two field elements, push `addmod(a, sub(r, b), r)` | +| `MUL` | Pop two field elements, push `mulmod(a, b, r)` | +| `MUL_CONST idx` | Pop one field element, multiply by constant `idx` | +| `NEG` | Pop one field element, push `r - a` | +| `STORE_ACC idx` | Pop one field element into accumulator/register `idx` | +| `END` | Stop interpretation | + +The exact opcode set should be chosen after inspecting the generated identity +IR. The best set is the smallest one that avoids excessive stack/register +traffic for common Halo2 identity patterns. + +## Program Encoding + +The generated verifier can pack the program into Solidity bytecode as a compact +constant blob. + +Possible encodings: + +1. One-byte opcodes with fixed-size immediate fields. +2. One-byte opcodes plus variable-width immediates for small indices. +3. Packed 16-bit or 32-bit instructions for simpler decoding. + +### Option 2 Benchmark: Common-Subexpression Elimination + +An experimental no-VM quotient CSE mode was added as: + +```rust +CodegenConfig { + quotient_inline_cse: true, + ..CodegenConfig::default() +} +``` + +This is the pure option 2 shape: it disables the quotient VM payload and renders +straight-line Yul for quotient evaluation. The generator parses the generated +quotient expression trees, counts repeated composite subexpressions, and stores +worthwhile repeats in a small memory temp area. Reuses become direct `mload` +references. There is no interpreter loop and no quotient bytecode program in the +VK. + +Command used: + +```sh +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ +scripts/run_ivc_bench.sh --skip-srs-download +``` + +Results against the byte-VM baseline below: + +| Mode | Verifier runtime | VK runtime | Total runtime | Quotient checkpoint | Total tx gas | +| --- | ---: | ---: | ---: | ---: | ---: | +| byte VM | 11,592 bytes | 19,712 bytes | 31,304 bytes | 1,029,944 gas | 2,484,560 gas | +| no-VM straight-line CSE | 57,487 bytes | 6,752 bytes | 64,239 bytes | 121,574 gas | 1,566,594 gas | + +This confirms the tradeoff: removing the VM cuts quotient-eval gas dramatically, +but it is not a contract-size optimization. The VK shrinks by 12,960 bytes +because the quotient program is gone, but the verifier grows by 45,895 bytes +because the arithmetic is emitted as straight-line code. Total deployed runtime +grows by 32,935 bytes, far beyond the 24 KiB verifier limit. + +For completeness, a separate VM-side CSE experiment is available behind: + +```rust +CodegenConfig { + quotient_vm_cse: true, + ..CodegenConfig::default() +} +``` + +That experiment keeps the quotient VM and CSEs inside the VM payload. It is not +pure option 2. + +This now uses the most relevant `snark-verifier` optimization for quotient +evaluation: a loader-style expression cache across the full quotient expression, +instead of rebuilding the cache for each identity. The generator builds one +global CSE plan for the VM suffix, emits the first selected repeated expression +into a temp slot, and replaces later appearances with `PUSH_TEMP`. + +Command used: + +```sh +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ +scripts/run_ivc_bench.sh --skip-srs-download +``` + +Results against the byte-VM baseline: + +| Mode | Verifier runtime | VK runtime | Total runtime | Quotient checkpoint | Total tx gas | +| --- | ---: | ---: | ---: | ---: | ---: | +| byte VM | 11,592 bytes | 19,712 bytes | 31,304 bytes | 1,029,944 gas | 2,484,560 gas | +| byte VM + global CSE | 11,707 bytes | 18,208 bytes | 29,915 bytes | 1,020,561 gas | 2,468,545 gas | + +The main verifier grows by 115 bytes because the VM needs the temp load/store +cases, but the VK payload shrinks by 1,504 bytes. Net deployed runtime shrinks +by 1,389 bytes and gas is roughly neutral/slightly better for this tree-decider +bench. + +### snark-verifier-style Sum/Product Folding + +The quotient evaluator also applies the same idea as `snark-verifier`'s +`sum_with_coeff_and_const` / `sum_products_with_coeff_and_const` helpers before +VM lowering: + +- flatten additive chains, +- fold constant terms and signs, +- fold `Scaled` coefficients into each term, +- emit `coeff * (lhs * rhs)` as one scaled product term when possible. + +This keeps the same quotient VM but gives the lowerer a tighter expression shape +and lets the existing fused add-mul opcodes trigger more often. + +Command used: + +```sh +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ +scripts/run_ivc_bench.sh --skip-srs-download +``` + +Results against the global-CSE VM run above: + +| Mode | Verifier runtime | VK runtime | Total runtime | Quotient checkpoint | Total tx gas | +| --- | ---: | ---: | ---: | ---: | ---: | +| byte VM + global CSE | 11,707 bytes | 18,208 bytes | 29,915 bytes | 1,020,561 gas | 2,468,545 gas | +| byte VM + global CSE + sum/product folding | 11,707 bytes | 18,208 bytes | 29,915 bytes | 1,013,113 gas | 2,461,095 gas | + +This saves 7,448 gas in the quotient checkpoint without changing deployed +runtime size. It is worthwhile, but it does not change the main conclusion: the +interpreter dispatch dominates, so larger gas wins require hybrid inlining, +more fused VM opcodes, or a sharded/helper straight-line evaluator. + +The other `snark-verifier` compact-backend idea is to move the whole program +into data-page contracts and run a generic interpreter. That is useful when a +single generated contract cannot fit EIP-170, but it is less attractive here +because the quotient-specific VM already keeps both the verifier and VK contracts +below 24 KiB individually and has a much smaller interpreter than the full +generic backend. + +### Option 3 Benchmark: Yul Helper Functions + +The no-VM CSE path can also call small Yul helper functions for repeated +arithmetic shapes: + +```rust +CodegenConfig { + quotient_inline_cse: true, + quotient_yul_helpers: true, + ..CodegenConfig::default() +} +``` + +The helper mode currently emits: + +- `q_add(a, b)` +- `q_mul(a, b)` +- `q_neg(a)` +- `q_madd(a, b, c)` for `a * b + c` +- `q_addmul(a, b, c)` for `a + b * c` + +Command used: + +```sh +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ +scripts/run_ivc_bench.sh --skip-srs-download +``` + +Results: + +| Mode | Verifier runtime | VK runtime | Total runtime | Quotient checkpoint | Total tx gas | +| --- | ---: | ---: | ---: | ---: | ---: | +| no-VM straight-line CSE | 57,487 bytes | 6,752 bytes | 64,239 bytes | 121,574 gas | 1,566,594 gas | +| no-VM CSE + Yul helpers | 55,970 bytes | 6,752 bytes | 62,722 bytes | 118,077 gas | 1,563,224 gas | + +Plain helpers reduce the verifier by 1,517 bytes and save 3,497 gas in the +quotient checkpoint, but the verifier is still far above the 24 KiB deployment +limit. This is useful if gas matters more than bytecode size, but it does not +solve deployability. + +A forced no-inline variant using one-trip loops in each helper reduced generated +source size further, but made `solc --via-ir` compile for roughly 25 minutes +without finishing on this verifier. That shape is not practical. + +### Hardcoded Structured-Loop Experiment + +A verifier-specific structured-loop mode was added as: + +```rust +CodegenConfig { + quotient_structured_loops: true, + ..CodegenConfig::default() +} +``` + +This is not a generic verifier path. It still emits a verifier tied to the +current VK, but it uses the VK metadata to hardcode the permutation quotient +family as loops over known memory tables: + +- permutation column evaluations, +- permutation sigma evaluations, +- `z_cur`, `z_next`, and non-final `z_last` evaluations, +- fixed `num_sets`, `num_cols`, and `chunk_len`. + +The remaining gate, lookup, and trash identities stay as direct generated Yul. +Simple-selector quotient targets use the same scaled memory accumulator scheme +as the quotient VM path: `q_sel_scale`, `q_sel_inv_scale`, and one final scaling +pass over `SELECTOR_ACC_MPTR`. + +Command used: + +```sh +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ +scripts/run_ivc_bench.sh --skip-srs-download +``` + +Result: + +| Mode | Verifier source | Verifier runtime | VK runtime | Total runtime | Quotient checkpoint | Total tx gas | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | +| hardcoded structured loops | 383,306 bytes | 51,955 bytes | 6,752 bytes | 58,707 bytes | 117,694 gas | 1,566,405 gas | + +The experiment verifies end to end, but it is only a small bytecode win because +the permutation quotient family is not the dominant source of deployed bytecode +for this VK. The more important takeaway is architectural: hardcoded loops are +viable for regular quotient families, but we need to loop or table-drive the +large gate and lookup identities before this can materially reduce the verifier +below the current straight-line size. + +### Packed 32-bit Instruction Encoding Benchmark + +An experimental packed-32 mode was added as: + +```rust +CodegenConfig { + quotient_encoding: QuotientProgramEncoding::Packed32, + quotient_limb_vm_ops: false, + ..CodegenConfig::default() +} +``` + +The default remains the byte-oriented VM because packed32 made the total deployed +system larger and more expensive for the current two-leaf IVC Keccak bench. + +Command used: + +```sh +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ +cargo test --release \ + --features evm,truncated-challenges,fewer-point-sets,solidity-gas-checkpoints \ + --test ivc_keccak_solidity ivc_final_keccak_solidity_e2e \ + -- --ignored --nocapture +``` + +Byte-VM baseline command: + +```sh +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ +cargo test --release \ + --features evm,truncated-challenges,fewer-point-sets,solidity-gas-checkpoints \ + --test ivc_keccak_solidity ivc_final_keccak_solidity_e2e \ + -- --ignored --nocapture +``` + +Results: + +| Encoding | Verifier runtime | VK runtime | Total runtime | Quotient checkpoint | Total tx gas | +| --- | ---: | ---: | ---: | ---: | ---: | +| byte VM | 11,592 bytes | 19,712 bytes | 31,304 bytes | 1,029,944 gas | 2,484,560 gas | +| packed32 | 11,179 bytes | 26,176 bytes | 37,355 bytes | 1,586,710 gas | 3,044,269 gas | + +Packed32 reduced the main verifier by 413 bytes, but increased the VK runtime by +6,464 bytes and increased the quotient-eval checkpoint by 556,766 gas. For this +circuit shape it is not a good default. + +A simple first version can use fixed-width instructions: + +```text +u8 opcode +u16 operand0 +u16 operand1 +``` + +This is not maximally dense, but it keeps the interpreter small and easy to +audit. Once the shape is proven, the generator can specialize common cases into +shorter instruction formats. + +## Interpreter Sketch + +The verifier would emit one Yul helper similar to: + +```yul +function evalQuotientProgram(programOffset, programLen, evalsPtr, challengesPtr, constsPtr) -> out { + let r := 21888242871839275222246405745257275088548364400416034343698204186575808495617 + let pc := programOffset + let end := add(programOffset, programLen) + let sp := 0 + + for { } lt(pc, end) { } { + let op := byte(0, mload(pc)) + pc := add(pc, 1) + + switch op + case 0x01 { + // LOAD_EVAL idx + } + case 0x02 { + // LOAD_CHALLENGE idx + } + case 0x03 { + // ADD + } + case 0x04 { + // MUL + } + case 0xff { + out := /* final accumulator */ + break + } + } +} +``` + +The real implementation should avoid an unbounded memory stack if possible. +Options include: + +- Use a small fixed stack in memory. +- Use a register file for identity accumulators. +- Generate stack-depth metadata and assert it at codegen time. +- Use specialized opcodes such as `MUL_ADD_ACC` to avoid stack churn. + +## Expected Tradeoff + +Benefits: + +- Large verifier bytecode reduction for circuits with many quotient terms. +- More stable contract size as circuit identity count grows. +- Keeps the verifier as a single contract if helper-contract splitting is not + desired. +- Easier to inspect quotient structure as data. + +Costs: + +- Higher verification gas due to interpreter dispatch and memory loads. +- More complex codegen and more careful auditing required. +- Debugging generated programs is less direct than reading straight-line Yul. +- Solc optimization may help less because the arithmetic is data-driven. + +## Implementation Plan + +1. Identify the internal representation used when emitting quotient evaluation. +2. Add a codegen mode for interpreted quotient evaluation. +3. Lower each quotient identity expression into compact instructions. +4. Emit constant tables for: + - Fixed field constants. + - Evaluation indices. + - Challenge indices. + - Optional register metadata. +5. Emit one Yul interpreter helper. +6. Replace the straight-line quotient evaluation block with a call into the + interpreter. +7. Keep the straight-line evaluator behind a feature flag or generator option + for comparison. +8. Add tests that compare interpreted and straight-line quotient scalars for the + same generated proof. +9. Benchmark: + - Verifier runtime bytecode size. + - Total deployment bytecode size. + - Verification gas. + - Solidity compile time. + +## Benchmark Commands + +For the IVC Keccak Solidity end-to-end benchmark: + +```sh +SRS_DIR=/path/to/midfall/zk_stdlib/examples/assets \ +cargo test --release \ + --features evm,truncated-challenges,fewer-point-sets,solidity-gas-checkpoints \ + --test ivc_keccak_solidity ivc_final_keccak_solidity_e2e \ + -- --ignored --nocapture +``` + +For the Poseidon-style Solidity gas checkpoint benchmark: + +```sh +cargo test --features evm,solidity-gas-checkpoints \ + --test poseidon_fixture poseidon_renders_compiles_and_verifies \ + -- --ignored --nocapture +``` + +Compare these metrics before and after enabling the interpreted quotient +evaluation: + +- `verifier runtime` +- `vk runtime` +- `total runtime` +- `total tx gas_used` +- The checkpoint labeled `batched identity numerator reconstruction` + +## Open Questions + +- Should the program be stored directly in verifier code, immutable data, or + calldata-like memory initialized during verification? +- What is the best minimal opcode set for Halo2 quotient identities? +- Can common subexpressions be recovered before lowering to the program? +- Should fixed-base accumulator terms be pre-collapsed before this stage so the + quotient program only handles the truly dynamic part? +- Is a register-machine interpreter smaller than a stack-machine interpreter + after Solidity/Yul compilation? +- Should this mode be the default only when the straight-line verifier exceeds a + bytecode threshold? + +## Recommendation + +Start with a simple fixed-width instruction format and a small Yul stack +interpreter. The first milestone should prove semantic equivalence and measure +bytecode reduction. After that, tune opcode density and add fused operations only +where the generated program profile shows real wins. diff --git a/proofs/solidity-verifier/docs/plans/quotient_size_reduction_plan.md b/proofs/solidity-verifier/docs/plans/quotient_size_reduction_plan.md new file mode 100644 index 000000000..2c5633a56 --- /dev/null +++ b/proofs/solidity-verifier/docs/plans/quotient_size_reduction_plan.md @@ -0,0 +1,367 @@ +# Quotient Size Reduction Plan + +Yes - the main issue is that the Rust implementation is an iterator over +identity expressions, while the Yul is an expression compiler that emitted the +whole AST as bytecode. The Rust function `partially_evaluate_identities` +evaluates gates, then chains permutation, lookup, and trash expressions into a +vector of `(Option, F)` results; simple selectors remain partially +linearized, while `None` identities are fully accumulated. ([GitHub][1]) + +For contract size, optimize in this order. + +## 1. Centralize the y-batching step + +This repeated block is everywhere: + +```yul +quotient_eval_numer := mulmod(quotient_eval_numer, y, r) +q_sel_scale := mulmod(q_sel_scale, y, r) +q_sel_inv_scale := mulmod(q_sel_inv_scale, q_y_inv, r) +... +``` + +Make every identity call one shared absorber: + +```yul +// Put these in scratch memory, not outer Yul locals, so the function +// does not need many parameters. +let QN_PTR := 0x7f00 +let QSCALE_PTR := 0x7f20 +let QISCALE_PTR := 0x7f40 +let QYINV_PTR := 0x7f60 + +mstore(QN_PTR, 0) +mstore(QSCALE_PTR, 1) +mstore(QISCALE_PTR, 1) +mstore(QYINV_PTR, q_y_inv) + +function absorb_identity(eval, selector_off, r, y, selector_acc_ptr, qn_ptr, qscale_ptr, qis_ptr, qyinv_ptr) { + let qn := mulmod(mload(qn_ptr), y, r) + let qscale := mulmod(mload(qscale_ptr), y, r) + let qis := mulmod(mload(qis_ptr), mload(qyinv_ptr), r) + + mstore(qn_ptr, qn) + mstore(qscale_ptr, qscale) + mstore(qis_ptr, qis) + + // selector_off == not(0) means fully evaluated / None selector + switch eq(selector_off, not(0)) + case 1 { + mstore(qn_ptr, addmod(qn, eval, r)) + } + default { + let p := add(selector_acc_ptr, selector_off) + mstore(p, addmod(mload(p), mulmod(eval, qis, r), r)) + } +} +``` + +Then each identity becomes: + +```yul +absorb_identity(eval, 0x80, r, y, SELECTOR_ACC_MPTR, QN_PTR, QSCALE_PTR, QISCALE_PTR, QYINV_PTR) +``` + +or for fully evaluated identities: + +```yul +absorb_identity(eval, not(0), r, y, SELECTOR_ACC_MPTR, QN_PTR, QSCALE_PTR, QISCALE_PTR, QYINV_PTR) +``` + +This removes a large amount of repeated bytecode and makes the code match the +Rust shape: "compute an eval, then batch it". + +Also loop these: + +```yul +for { let off := 0 } lt(off, 0x140) { off := add(off, 0x20) } { + mstore(add(SELECTOR_ACC_MPTR, off), 0) +} +... +let final_scale := mload(QSCALE_PTR) +for { let off := 0 } lt(off, 0x140) { off := add(off, 0x20) } { + mstore(add(SELECTOR_ACC_MPTR, off), mulmod(mload(add(SELECTOR_ACC_MPTR, off)), final_scale, r)) +} +``` + +## 2. Do not emit every gate polynomial as code + +The huge blocks like the 7-limb / wide 7-limb arithmetic are the real bytecode +killer. They should become data plus a small evaluator. + +You have many repeated shapes: + +```yul +q_limb7(a_0, ..., a_6) +q_limb7_wide(a_0, ..., a_6) +sum coeff[i][j] * lhs[i] * rhs[j] +sum coeff[i] * pow5(a[i]) +``` + +These should be generated as compact term tables, not unrolled code. + +For example, replace this style: + +```yul +let var48 := mulmod(a_2, a_0_next_1, r) +let var49 := mulmod(var6, var48, r) +let var50 := addmod(var47, var49, r) +... +``` + +with a generic bilinear evaluator: + +```yul +function eval_bilinear7(lhs_ptr, rhs_ptr, coeff_ptr, r) -> acc { + acc := 0 + + for { let i := 0 } lt(i, 7) { i := add(i, 1) } { + let li := mload(add(lhs_ptr, shl(5, i))) + + for { let j := 0 } lt(j, 7) { j := add(j, 1) } { + let coeff := mload(add(coeff_ptr, shl(5, add(mul(i, 7), j)))) + let rj := mload(add(rhs_ptr, shl(5, j))) + acc := addmod(acc, mulmod(coeff, mulmod(li, rj, r), r), r) + } + } +} +``` + +You can then have two coefficient tables: + +```text +LIMB7_MUL_COEFFS +LIMB7_WIDE_MUL_COEFFS +``` + +and reuse the same evaluator for all those 200+ line blocks. + +This is probably the biggest win. + +## 3. Encode custom gates as a small instruction stream + +For maximum size reduction, generate something closer to an interpreter: + +```text +IDENTITY { + selector_offset: 0x80, + terms: [ + CONST * A0 * A0_NEXT, + CONST * A0 * A1_NEXT, + ... + CONST, + -A7_NEXT * CONST, + ... + ] +} +``` + +Then one Yul evaluator handles sparse products: + +```yul +function eval_terms(ptr, end, r) -> acc { + acc := 0 + + for { } lt(ptr, end) { } { + let coeff := mload(ptr) + ptr := add(ptr, 0x20) + + let n_factors := mload(ptr) + ptr := add(ptr, 0x20) + + let term := coeff + for { let k := 0 } lt(k, n_factors) { k := add(k, 1) } { + let value_ptr := mload(ptr) + ptr := add(ptr, 0x20) + term := mulmod(term, mload(value_ptr), r) + } + + acc := addmod(acc, term, r) + } +} +``` + +Then the generated code per identity is just: + +```yul +let eval := eval_terms(TABLE_PTR, TABLE_END, r) +absorb_identity(eval, 0x80, r, y, SELECTOR_ACC_MPTR, QN_PTR, QSCALE_PTR, QISCALE_PTR, QYINV_PTR) +``` + +This trades gas for size, but for a verifier near EIP-170, it is the right +trade. EIP-170 rejects creation if the returned contract code is larger than +`MAX_CODE_SIZE = 0x6000`, i.e. 24,576 bytes. ([Ethereum Improvement +Proposals][2]) + +## 4. Move constants into tables, preferably not duplicated as PUSH32 + +Every unique `PUSH32` costs about 33 bytes before surrounding opcodes. Your code +repeats many large constants across narrow/wide/current/next/prev variants. + +Options: + +### Same contract, compact table + +Use one table copied into memory once: + +```yul +codecopy(CONST_TABLE_PTR, dataoffset("constants"), datasize("constants")) +``` + +This still counts toward runtime bytecode if the data is in the deployed code, +but it removes duplication. + +### External constants contract + +Put large coefficient tables in a separate deployed blob / constants contract +and load with `EXTCODECOPY`. This reduces the main verifier's runtime size at +the cost of extra gas and an extra deployment dependency. + +### External libraries + +If you split evaluators into external libraries, their code is separate from the +verifier's EIP-170 limit. Be careful: internal library functions are normally +compiled into the caller, but linked external libraries remain separate. +Solidity's compiler docs describe library placeholders/linking for separate +library bytecode. ([docs.soliditylang.org][3]) + +## 5. Loop the permutation packing too + +This part: + +```yul +mstore(add(q_perm_vals, 0x0), mload(0x5e00)) +mstore(add(q_perm_sigmas, 0x0), mload(0x6020)) +... +``` + +should be table-driven: + +```yul +let val_srcs := 0x8000 +let sig_srcs := 0x8240 + +// Fill val_srcs/sig_srcs once using codecopy/table, +// then: +for { let i := 0 } lt(i, 18) { i := add(i, 1) } { + let off := shl(5, i) + mstore(add(q_perm_vals, off), mload(mload(add(val_srcs, off)))) + mstore(add(q_perm_sigmas, off), mload(mload(add(sig_srcs, off)))) +} +``` + +The nested permutation product loops are already close to the right form. The +packing before them is still too unrolled. + +## 6. Trash block: convert to matrix-vector + Horner + +The trash block is a perfect candidate: + +```text +row_0 = f0 + c00*a0 + c01*a1 + c02*pow5(a2) + ... +row_1 = f1 + c10*a0 + c11*a1 + c12*pow5(a2) + ... +... +batched = (((row0 * tau + row1) * tau + row2) ...) +``` + +Precompute: + +```yul +mstore(pow_ptr + 0x00, q_pow5(a_2)) +mstore(pow_ptr + 0x20, q_pow5(a_3)) +... +``` + +Then: + +```yul +function eval_trash_rows(row_coeffs, base_terms, rows, cols, tau, r) -> acc { + acc := 0 + + for { let row := 0 } lt(row, rows) { row := add(row, 1) } { + let row_eval := 0 + + for { let col := 0 } lt(col, cols) { col := add(col, 1) } { + let coeff := mload(add(row_coeffs, shl(5, add(mul(row, cols), col)))) + let val := mload(add(base_terms, shl(5, col))) + row_eval := addmod(row_eval, mulmod(coeff, val, r), r) + } + + acc := addmod(mulmod(acc, tau, r), row_eval, r) + } +} +``` + +This replaces hundreds of lines of constants and repeated `mulmod/addmod`. + +## 7. Compiler settings: optimize for size, not runtime + +Use low optimizer runs: + +```json +{ + "optimizer": { + "enabled": true, + "runs": 1 + }, + "evmVersion": "shanghai" +} +``` + +Solidity's docs explicitly describe `--optimize-runs=1` as producing shorter +but more expensive code, while higher runs produce longer but more gas-efficient +code. ([docs.solidity.org][4]) + +Also compile for at least `shanghai` if your target chain supports it, because +`PUSH0` can reduce code size and gas. The Solidity docs note smaller code size +and gas savings from `push0` in the Shanghai target. ([docs.soliditylang.org][3]) + +One warning: the optimizer can inline functions. Solidity's Yul optimizer +includes function inlining, and its docs note that inlining can increase code +size in many cases. ([docs.solidity.org][4]) So after introducing helpers like +`absorb_identity`, check the optimized IR/bytecode. If the helper is inlined +back into every identity, either lower runs further, make it less attractive to +inline, or customize the Yul optimization sequence. + +## Recommended target structure + +Refactor the generated Solidity/Yul into this pipeline: + +```text +1. Load all evaluations into memory arrays: + advice_cur, advice_next, advice_prev, fixed, instance, permutation, lookup, trash + +2. Initialize: + quotient_eval_numer = 0 + selector_acc[0..9] = 0 + q_sel_scale = 1 + q_sel_inv_scale = 1 + q_y_inv = y^-1 + +3. For each gate identity: + eval = eval_terms(...) or eval_bilinear7(...) + absorb_identity(eval, selector_offset) + +4. Permutation: + existing loops, but table-drive the packing + +5. Lookup: + keep loops, simplify length-1 cases manually + +6. Trash: + matrix-vector evaluator + Horner in tau + +7. Final: + scale selector_acc by q_sel_scale + QUOTIENT_EVAL_MPTR = -quotient_eval_numer +``` + +The most important change is: stop generating arithmetic expressions as Yul +statements. Generate a compact table/instruction stream and a few reusable +evaluators. That is the Solidity equivalent of the Rust iterator design, and it +is the route that will materially reduce deployed bytecode. + +[1]: https://raw.githubusercontent.com/EYBlockchain/midfall/f6feca8b33fa62798905d668961a35f9736302d2/proofs/src/plonk/mod.rs "raw.githubusercontent.com" +[2]: https://eips.ethereum.org/EIPS/eip-170 "EIP-170: Contract code size limit" +[3]: https://docs.soliditylang.org/en/latest/using-the-compiler.html?highlight=optimize-runs "Using the Compiler - Solidity 0.8.36-develop documentation" +[4]: https://docs.solidity.org/en/latest/internals/optimizer.html "The Optimizer - Solidity 0.8.36-develop documentation" diff --git a/proofs/solidity-verifier/docs/plans/spec-migration.md b/proofs/solidity-verifier/docs/plans/spec-migration.md new file mode 100644 index 000000000..f046988df --- /dev/null +++ b/proofs/solidity-verifier/docs/plans/spec-migration.md @@ -0,0 +1,119 @@ +## Scope + +Migrate the codegen-based Solidity verifier in `/Users/Julien.Coolen/halo2-solidity-verifier-exp` to consume `midnight-proofs` (path: `../midfall/proofs`) instead of vendored halo2-proofs v0.4. Target a single end-to-end success: verifying the existing `proof.bin / vk.bin / instance.be` fixtures at `/Users/Julien.Coolen/midfall/proofs/solidity-verifier/fixtures/poseidon/` produced by the keccak-branch poseidon example. + +This is a multi-layer rewrite. The halo2-proofs v0.4 verifier and midnight-proofs verifier differ on at least these axes: + +| Layer | halo2-proofs (current target) | midnight-proofs (new target) | +|------------------------|--------------------------------------------|--------------------------------------------------------------------| +| Lookup arg | Halo2 grand-product lookup | LogUp (multiplicities + per-chunk helpers + accumulator) | +| Trash arg | -- | trash::Argument (selector + constraint exprs + single Z eval) | +| PCS | BDFG21 / GWC19 (single nu + W openings) | Midnight multi-prepare (x1, x2, f_com, x3, q_evals, x4, pi) | +| Transcript hash | Keccak256, raw byte concat + 0x01 cont | Keccak256 with domain sep + PREFIX_COMMON/CHALLENGE + 64-byte squeeze + uniform-bytes reduction | +| G1 in transcript | Uncompressed EIP-2537 padded (128 bytes) | Compressed BLS (48 bytes) | +| Verifier flow phases | advice -> theta -> lookup_perm -> beta/gamma -> perm_z -> y -> quotient -> x -> evals | advice/challenges -> theta -> mults -> beta/gamma -> perm_z -> lookup_helpers/acc -> trash_challenge -> trashcans -> y -> quotient -> x -> evals | +| Permutation product | At each rotation | sets, with per-set queries (cur, next, last) | +| Number of advice phases| Per ConstraintSystem::phases | Same shape; midnight-proofs builds on halo2 v0.4 here | +| committed-instances | -- | feature flag (we'll keep nb_committed_instances=0 for poseidon) | + +## Layered execution plan + +I'll deliver the migration in N self-contained steps, committing each so we can ship-and-iterate. Per step I'll re-run `cargo check` and the relevant tests. + +### Step 1 - Dependency switch + minimal type rebind +- Replace vendored `halo2_proofs/halo2_middleware/halo2_backend` with `midnight-proofs = { path = "../midfall/proofs", features = ["keccak-transcript"] }` and `midnight-curves = "0.2.0"`. +- Drop `halo2curves` (midnight-curves carries BLS12-381 directly). +- Rebind everywhere we use `halo2_proofs::halo2curves::bls12381::*` to `midnight_curves::*` (Fq, G1Affine, G1Projective, Bls12). +- Rebind `VerifyingKey` to `VerifyingKey>`, `ParamsKZG` to `ParamsKZG`. +- Move `vendor/halo2/` out of the build path (keep on disk, not in Cargo). +- Outcome: `cargo check` red until later steps; we get the type universe pinned. + +### Step 2 - Transcript replacement +- Throw away the current `src/transcript.rs` (writes uncompressed EIP-2537 G1, single-keccak squeeze). +- Implement a `Keccak256VerifierTranscript` matching `midnight_proofs::transcript::CircuitTranscript` byte-for-byte: + - init: `Keccak256::new().update("Domain separator for transcript")` + - common(input): hasher.update([1u8]); hasher.update(input) + - common(G1): compressed 48-byte encoding (`::to_bytes`) + - squeeze: clone+update([0])+finalize() || clone+update([1])+finalize() -> 64 bytes; reseed hasher with those bytes. + - sample Fq: `from_uniform_bytes(64)` (which is `LE_lo + LE_hi * 2^256 mod r`). +- Mirror this in Yul (see Step 5). + +### Step 3 - ConstraintSystemMeta rewrite +- `src/codegen/util.rs::ConstraintSystemMeta::new` currently inspects halo2_proofs' lookups; replace its lookup section with midnight-proofs' `cs.lookups()` (BatchedArgument with chunks, helpers, multiplicities). +- Add a trashcan section: `cs.trashcans()` (Argument with selector + constraint expressions). +- Track `num_committed_instances` (keep at 0 for poseidon); track `cs.num_simple_selectors()` (midnight-proofs filters fixed evals on this). +- New per-row counts: `lookup_chunks_per_arg[]`, `trashcan_count`, `permutation_chunks` (=cols.div_ceil(degree-2)). +- `proof_len` calculation now needs: advices, multiplicities, perm prod commitments, lookup helpers + accumulators, trashcans, quotient limbs, evals (committed-inst evals + advice + fixed-non-simple + perm-common + perm-set + lookup-evals + trash), f_com, q_evals (one per point set), pi. + +### Step 4 - Evaluator rewrite (partial-eval) +- `src/codegen/evaluator.rs` currently emits Yul for halo2 lookup constraints. Replace `lookup_computations` with a logup emitter that writes: + - For each lookup (per proof): boundary `(l_0 + l_last)*Z` + - Per chunk: `h(x) * prod_j(f_j(x)+beta) - sum_j prod_{k!=j}(f_k(x)+beta)` where each `f_j` is theta-compressed + - Accumulator: `(Z_next - Z - selector*sum_h)*(t+beta) + m` times `(1 - l_last - l_blind)` +- Add `trashcan_computations`: per trashcan, `compress_constraints(theta) - (1-q)*trash_eval`. +- Gate emitter unchanged shape, but now reads through midnight-proofs `Expression`. + +### Step 5 - PCS emitter rewrite +- Replace `src/codegen/pcs.rs` with a `multi_prepare` emitter: + - `construct_intermediate_sets`: bucket queries by point sets (in order: advice rotations, perm cur/next/last, lookup x/x_next, trashcan x, fixed rotations, perm common at x, lin com at x). + - `q_coms`: per set, MSM-fold commitments by `x1` powers + - `q_eval_sets`: per set, eval_set inner product with `x1` powers + - Sort by ascending point-set cardinality (matches midnight-proofs `(len, i)` total order) + - Read `f_com` from proof + - Read `len(point_sets)` `q_evals` at `x3` + - Compute `f_eval` via Horner over reverse(point_sets) using `lagrange_interpolate(points, evals)` evaluated at `x3` (Yul: precompute `den = prod_i (x3 - point_i)`, then `(proof_eval - r_eval) * den_inv`) + - Final commitment = `MSM(q_coms[0], 1) + MSM(q_coms[i], x4^i) + MSM(f_com, x4^(len))` + - Final eval = `inner_product(q_evals, x4_powers) + f_eval * x4^len` + - Read `pi`. PAIRING_LHS = pi. PAIRING_RHS = (final_com - v*G1) + x3*pi. + +### Step 6 - Yul template rewrite (Halo2Verifier.sol) +- Replace transcript helpers (`squeeze_challenge`, `squeeze_challenge_cont`, `read_g1_point`) with new midnight-proofs equivalents: + - `read_g1_compressed`: read 48 bytes, hash compressed bytes into transcript, then call modexp+sqrt to expand to 128-byte EIP-2537 form for arithmetic. + - `squeeze_challenge_64`: emit two `keccak(state || PREFIX_CHALLENGE || 0x00)` and `keccak(state || PREFIX_CHALLENGE || 0x01)` calls, concat to 64 bytes, reduce via `(lo + hi*2^256) mod r`. Reseed state with the 64 bytes. +- Add LogUp loops in the verifier flow: + ``` + for each proof: read num_lookups multiplicities (G1 each) + squeeze beta, gamma + for each proof: read perm_chunks commitments + for each proof: for each lookup: read num_chunks helpers + 1 accumulator + squeeze trash_challenge + for each proof: read num_trashcans commitments + squeeze y + ``` +- Read evaluations in midnight-proofs order (committed instance > advice > fixed-non-simple > perm-common > perm-set > lookup > trash). + +### Step 7 - VK template (Halo2VerifyingKey.sol) +- Add new VK constants: `cs_degree`, `num_simple_selectors`, `num_logup_lookups`, `lookup_chunks[]`, `num_trashcans`, `permutation_chunks`, `permutation_cols`. +- Embed permutation column metadata (per-column kind + query index). +- Embed gate/lookup/trashcan expression bytecode (NEW: we'll need a small bytecode interpreter, not a per-gate Yul emitter, OR continue with Yul-emitting if we're willing to spend the tokens). My strong recommendation: keep Yul-emitting (current architecture) for poseidon and only embed the few cs scalars we need; lookups/trashcans become Yul-emitted expression evals just like gates today. + +### Step 8 - Verification driver +- New example/test (ideally `examples/poseidon_verify.rs`) that: + 1. Reads `proof.bin`, `vk.bin`, `instance.be` from the midfall fixture dir. + 2. Reconstructs `VerifyingKey` via `VerifyingKey::from_bytes` (or `read_from_cs`) using the same `ZkStdLib`/`PoseidonExample` `ConstraintSystem` shape. + 3. Calls `SolidityGenerator::new(...).render(...)` to produce Halo2Verifier.sol. + 4. Compiles via `compile_solidity` (already wired through `solc`). + 5. Deploys + calls `verifyProof(proof_padded, [instance])` on revm with the Prague EVM. + 6. Asserts true. + 7. Also runs `midnight_proofs::plonk::prepare(...)` on the same proof and asserts it succeeds (cross-check). + +### Step 9 - Soundness checks +- Fixture-driven negative tests (re-using existing `adversarial_fixtures.bin`): + - Mutated proof bytes -> reject + - Mutated public input -> reject + - Mutated VK -> reject (codehash pin) + - Wrong commitment ordering -> reject + +## Risks / open questions + +- **Fixture ConstraintSystem reconstruction.** `VerifyingKey::from_bytes` requires the matching `Circuit` type. For the poseidon example that means we'd need to import `midnight_zk_stdlib::PoseidonExample` (the dev dep transitively pulls midnight-circuits). I'll add it as a dev-dep alongside midnight-zk-stdlib. The fixture vk.bin was produced by the keccak-branch generate_ivc / generate.rs binaries on `verifier-contract`; if its serde format has drifted on `keccak`, I'll regenerate it from the live poseidon.rs example as part of Step 8. + +- **Compressed G1 decompression in Solidity.** EIP-2537 precompiles consume *uncompressed* form, so before any `G1MSM`/`G1ADD` we need to decompress 48-byte BLS12-381 points in Yul. This requires a 381-bit modexp (sqrt) using `modexp` precompile (0x05) over `(lo, hi)` halves. The verifier-contract branch already has a working implementation (`_g1CompressedToEip2537`). I'll port that to Yul (~200 lines) as a helper. + +- **Order of magnitude.** This is a multi-day rewrite. I'll commit each step so progress is checkpointable. End-to-end verification of the poseidon fixture inside a single session is unlikely; what's achievable in one pass is Steps 1-3 (dep + transcript + cs meta) with `cargo check` green and a unit test pinning the new transcript byte-for-byte against a midnight-proofs round-trip. + +## What I'll deliver in this first session + +If you approve, I'll execute Steps 1-3 in this session (dep switch + transcript + ConstraintSystemMeta), commit the result, and write a follow-up plan for Steps 4-9 in a `MIGRATION.md`. End-to-end fixture validation (Step 8) will require a follow-up session. + +If you'd rather I attempt all of it in one pass even at the cost of leaving compile errors in some files (so you can review the architecture in one go), I'll do that instead -- pick from the options below. diff --git a/proofs/solidity-verifier/docs/reference/ASKAMA_TEMPLATE_RUST_MAPPING.md b/proofs/solidity-verifier/docs/reference/ASKAMA_TEMPLATE_RUST_MAPPING.md new file mode 100644 index 000000000..efceaa81e --- /dev/null +++ b/proofs/solidity-verifier/docs/reference/ASKAMA_TEMPLATE_RUST_MAPPING.md @@ -0,0 +1,438 @@ +# Askama Template And Rust Verifier Mapping + +This note explains how the generated Solidity/Yul artifacts map back to the +Midfall Rust verifier in `../midfall/proofs/src/plonk` and to the local +lowering modules that feed the Askama templates. + +The short version: Askama is the last mile, not the planner. Rust code in +`src/lowering` computes the proof layout, memory layout, VK payload, quotient +program, and PCS emitter blocks. The templates render those already-planned +facts into Solidity/Yul with mostly fixed control flow. + +## Template Inventory + +| Template | Askama struct | Rendered artifact | Primary Rust source of truth | +| --- | --- | --- | --- | +| `templates/contracts/Halo2Verifier.sol` | `src/lowering/render/models.rs::Halo2Verifier` | Main verifier contract and `verifyProof` ABI | `../midfall/proofs/src/plonk/verifier.rs::{parse_trace,verify_algebraic_constraints,prepare}` | +| `templates/contracts/Halo2VerifyingKey.sol` | `src/lowering/render/models.rs::Halo2VerifyingKey` | Data-only VK runtime payload contract | `../midfall/proofs/src/plonk/mod.rs::VerifyingKey` | +| `templates/contracts/Halo2QuotientEvaluator.sol` | `src/lowering/render/models.rs::Halo2QuotientEvaluator` | Optional split quotient numerator evaluator | `../midfall/proofs/src/plonk/mod.rs::partially_evaluate_identities` and `linearization/verifier.rs::compute_linearization_commitment` | +| `templates/partials/quotient_numerator/QuotientNumeratorBlock.yul` | included by the verifier/evaluator templates | Generated numerator reconstruction body | `../midfall/proofs/src/plonk/{mod.rs,permutation.rs,logup.rs,trash.rs}` | +| `templates/partials/quotient_numerator/QuotientHelpers.yul` | included by `QuotientNumeratorBlock.yul` | Optional arithmetic helpers for generated quotient snippets | Local lowering helpers in `src/lowering/quotient_numerator/yul_emit.rs` and `src/lowering/quotient_numerator/vm/mod.rs` | + +The main generator entry points are in `src/builder/` and `src/lowering/artifacts.rs`: + +- `render(RenderOptions { vk: RenderVk::Embedded, .. })` produces an + embedded-VK verifier. +- `render(RenderOptions { vk: RenderVk::Separate, .. })` produces + `Halo2Verifier` plus `Halo2VerifyingKey`. +- `render_quotient_evaluator(diagnostics)` produces the split quotient + evaluator first. +- `render(RenderOptions { quotient: RenderQuotient::ExternalPinned { .. }, .. })` + produces verifier/VK/evaluator artifacts where the verifier pins the + evaluator runtime length and codehash. + +## Data Flow Into Askama + +Askama structs are deliberately plain data bags: + +1. `SolidityGenerator::try_new` validates the supported Midfall shape and + derives `ConstraintSystemMeta`. +2. `src/lowering/abi/proof.rs` derives the exact calldata sections for proof + commitments, scalar evaluations, KZG `q_evals`, `f_com`, and `pi`. +3. `src/lowering/layout/memory.rs` assigns stable memory addresses for VK words, + challenges, decoded evaluations, commitments, quotient state, PCS scratch, + and optional accumulator state. +4. `src/lowering/layout/vk_payload.rs` validates the VK payload section map: header, + quotient constants, quotient program, fixed commitments, and permutation + commitments. +5. `src/lowering/quotient_numerator/yul_emit.rs` lowers gates, permutation, LogUp, and trash + expressions into scalar Yul fragments. +6. `src/lowering/quotient_numerator/vm/mod.rs` optionally turns those fragments into a + compact quotient program plus native callback markers. +7. `src/lowering/kzg/mod.rs` simulates Midfall KZG `multi_prepare` and emits the + final PCS Yul blocks. +8. `src/lowering/render/models.rs` packages all of the above into Askama structs. +9. The templates render Solidity/Yul without recomputing protocol layout. + +That separation is intentional. When a template contains a numeric memory +constant, byte offset, or loop bound, it should already have been checked by a +Rust-side layout test or generator assertion. + +## `Halo2Verifier.sol` Section Map + +`Halo2Verifier.sol` is the on-chain version of: + +```text +parse_trace(...) +verify_algebraic_constraints(...) +CS::multi_prepare(...) +guard.verify(params) +``` + +with Midfall transcript and KZG details rendered directly into Yul. + +| Generated section | What it does | Midfall Rust mapping | Local codegen owner | +| --- | --- | --- | --- | +| Contract header, pinned dependencies, constants | Declares VK/evaluator codehash pins, calldata offsets, memory addresses, field/precompile constants | `plonk/mod.rs::VerifyingKey` metadata and KZG verifier params | `template.rs::{Halo2Verifier,TemplateConstants}`, `layout.rs`, `memory.rs`, `artifact.rs` | +| Constructors and EIP-2537 smoke tests | Checks G1ADD, G1MSM, and pairing precompiles, then pins VK and optional quotient evaluator | Deployment-only safety wrapper around the verifier params | `templates/contracts/Halo2Verifier.sol`, `layout::precompile` | +| `verifyProof` ABI head checks | Confirms ABI offsets, generated proof length, instance length, and calldata end | `verifier.rs` instance-shape checks before transcript work | `proof_layout.rs`, `generator.rs` | +| Helper functions | Implements scalar inverse, transcript append/squeeze, G1 validation/copy, G1MSM/g1add/pairing wrappers, batch inversion, accumulator decoding, trace/gas hooks | `transcript/*`, `poly/domain.rs::l_i_range`, KZG verifier helpers | `templates/contracts/Halo2Verifier.sol`, `TemplateConstants` | +| VK loading | Either emits `mstore` constants or `extcodecopy`s the payload from the pinned `INVALID || VK payload` runtime, then validates accumulator header fields | `vk.hash_into(transcript)` uses `vk.transcript_repr`; fixed/permutation commitments are verifier key data | `generate_vk`, `Halo2VerifyingKey`, `VkPayloadLayout` | +| VK digest and public input transcript absorption | Absorbs `vk_digest`, committed-instance identity commitment, public input length, and public input scalars | `verifier.rs::parse_trace`, from `vk.hash_into` through instance `transcript.common` calls | `proof_layout.rs::TranscriptBufferLayout`, `common_word`, `common_uncompressed_g1` | +| Per-user-phase advice reads | Reads and absorbs advice commitments by phase, then squeezes user challenges | `parse_trace`: phase loop over advice commitments and `challenge_phase` | `Protocol` metadata, `UserPhase` | +| `theta`, lookup multiplicities | Squeezes `theta`, then reads lookup multiplicity commitments | `parse_trace`: `theta`, `ChunkedArgument::read_multiplicities` | `proof_layout.rs`, `logup/verifier.rs` mapping | +| `beta`, `gamma`, permutation products | Squeezes permutation challenges and reads permutation product commitments | `parse_trace`: `beta`, `gamma`, `Argument::read_product_commitments` | `permutation/verifier.rs` mapping | +| Lookup helper/accumulator commitments | Reads each LogUp helper commitment and accumulator commitment | `CommittedMultiplicities::read_commitment` | `proof_layout.rs::ProofLookupCommitmentsLayout` | +| `trash_challenge`, trash commitments | Squeezes trash challenge unconditionally, then reads trash commitments when present | `parse_trace`: trash challenge and `trash::Argument::read_committed` | `trash/verifier.rs` mapping | +| `y`, quotient commitment(s) | Squeezes identity-batching challenge and reads quotient commitment(s) | `verify_algebraic_constraints`: `read_n(transcript, nb_quotient_coms)` before `x` | `proof_layout.rs`, `memory.rs` | +| `x` and evaluation scalars | Squeezes opening challenge, range-checks proof evals, stores them in `REVERSED_EVALS_MPTR`, and absorbs them | `verify_algebraic_constraints`: committed-instance evals, advice evals, fixed evals, permutation/common evals, lookup evals, trash evals | `Protocol` eval order, `proof_layout.rs`, `evaluator.rs` | +| `x1`, `x2`, `f_com`, `x3`, `q_evals`, `x4`, `pi` | Completes Midfall KZG transcript for multi-opening proof | KZG `multi_prepare` in `../midfall/proofs/src/poly/kzg` | `pcs.rs`, `proof_layout.rs` | +| Lagrange and instance evaluation | Computes `x^n`, `(x^n-1)^-1`, `l_last`, `l_blind`, `l_0`, and the public instance evaluation | `verify_algebraic_constraints`: `domain.l_i_range` and `compute_inner_product` | `templates/contracts/Halo2Verifier.sol`, `memory.rs` | +| Batched identity numerator reconstruction | Reconstructs `nu_y(x)` from the claimed evals and stores the linearization expected scalar `-nu_y(x)` | `plonk/mod.rs::partially_evaluate_identities`; trace loop for `quotient_numerator` and selector folds | `evaluator.rs`, `quotient/mod.rs`, `QuotientNumeratorBlock.yul` | +| Linearization scalar prep | Computes `x_split = x^(n-1)` and `1 - x^n`; prepares selector buckets and quotient-limb scalars | `linearization/verifier.rs::compute_linearization_commitment` | `templates/contracts/Halo2Verifier.sol`, `pcs.rs` | +| PCS computation blocks | Constructs point sets, folds evaluations/commitments, computes `f_eval`, `v`, final commitment, and pairing inputs | `CS::multi_prepare` and KZG final guard verification | `src/lowering/kzg/mod.rs` | +| Public accumulator pairing batch | Decodes public IVC accumulator points/scalars and batches the accumulator pairing equation with KZG | Repository-specific IVC extension around Midfall KZG | `templates/contracts/Halo2Verifier.sol`, `layout::accumulator` | +| Final pairing | Calls EIP-2537 pairing on the KZG or batched KZG+accumulator equation | `poly/kzg/msm.rs::DualMSM::check` | `ec_pairing` helper, `pcs.rs` | +| Trace and gas checkpoints | Emits trace ids and gas deltas for Rust/Solidity equivalence and profiling | `plonk/solidity_trace.rs` trace ids | Template trace/gas flags and bench scripts | + +Two naming caveats matter while reading the generated Solidity: + +- `QUOTIENT_EVAL_MPTR` is a legacy name. It stores the linearization expected + scalar `-nu_y(x)`, not a trusted quotient evaluation `h(x)`. +- `PAIRING_LHS_MPTR` and `PAIRING_RHS_MPTR` follow the internal dual-MSM naming. + The final `ec_pairing` call swaps them into EIP-2537 pairing argument order. + +## `Halo2VerifyingKey.sol` + +The VK template emits a constructor that returns `INVALID || payload` as runtime +bytecode. Runtime byte `0` is an unconditional `INVALID` opcode so direct calls +cannot execute payload bytes as EVM instructions. The verifier pins the full +prefixed runtime length/codehash but loads only the payload with: + +```yul +extcodecopy(vk, VK_MPTR, 0x01, vk_payload_len) +``` + +Its payload layout is: + +1. Header words: `vk_digest`, domain constants, accumulator metadata, KZG bases. +2. Optional compact quotient VM constant table. +3. Optional compact quotient VM bytecode words. +4. Fixed-column commitments. +5. Permutation commitments. + +This mirrors the Rust `VerifyingKey` inputs used by `vk.hash_into(transcript)` +and by `verify_algebraic_constraints`. The local `VkPayloadLayout` keeps the +payload section map append-only so `Halo2VerifyingKey.bytes()`, the prefixed +runtime length/codehash, `extcodecopy`, and tests all agree. + +Important optimisation: quotient constants and bytecode live in the pinned VK +payload rather than as verifier `PUSH32`/`mstore` immediates. That reduces main +verifier runtime size while keeping the program covered by the VK codehash. + +## `Halo2QuotientEvaluator.sol` + +The split evaluator is a specialized scalar helper for the expensive numerator +reconstruction section. It receives a raw verifier-memory frame, rehydrates it +to the same absolute memory addresses, includes `QuotientHelpers.yul` and +`QuotientNumeratorBlock.yul`, then returns: + +```text +word 0: magic/version +word 1: linearization_expected_eval = -nu_y(x) +word 2..: simple-selector accumulator buckets +``` + +This contract maps to the scalar side of: + +- `plonk/mod.rs::partially_evaluate_identities` +- `plonk/linearization/verifier.rs::compute_linearization_commitment` +- `plonk/permutation.rs::expressions` +- `plonk/logup.rs` / `plonk/logup/verifier.rs` +- `plonk/trash.rs::Evaluated::expressions` + +It does not read proof calldata, sample challenges, evaluate `h(x)`, or check +commitments. The main verifier has already done transcript parsing and later +binds the returned scalar to the quotient commitment(s) through KZG. + +Because the main verifier calls the evaluator with `STATICCALL`, evaluator-local +trace hooks are logless in split mode. The main verifier still traces proof +scalars, `q_evals`, selector folds returned from the evaluator, and the final +reconstructed scalar. Monolithic trace tests can additionally compare internal +quotient identity trace ids because they do not cross a `STATICCALL` boundary. + +## `QuotientNumeratorBlock.yul` + +This included Yul block is the concrete lowering of Midfall identity evaluation: + +```text +for identity in partially_evaluate_identities(...): + if identity is fully evaluated: + quotient_numerator = quotient_numerator * y + identity(x) + if identity is gated by a simple selector: + selector_bucket[selector] += y_power * identity(x) + +QUOTIENT_EVAL_MPTR = -quotient_numerator +``` + +The block preserves the Rust identity order: + +1. Gate identities from `vk.cs.gates`. +2. Permutation identities from `plonk/permutation.rs`. +3. LogUp identities from `plonk/logup.rs`. +4. Trash identities from `plonk/trash.rs`. + +The direct, VM, and native-callback paths are all different encodings of this +same ordered stream. Each identity consumes exactly one position in the global +`y` batch, including simple-selector identities. + +### Assembly Section Walkthrough + +`QuotientNumeratorBlock.yul` is included inside an existing Solidity/Yul +function body. It assumes the verifier/evaluator has already populated fixed +memory addresses for transcript challenges, proof evaluations, Lagrange values, +public-instance evaluation, quotient VM payload pointers, and selector bucket +scratch. + +The top-level template shape is: + +```text +{ + match quotient_program: + Some(program) => compact VM/native/direct-hybrid path + None => legacy direct numerator path +} +``` + +The compact path is the production path. Its generated assembly sections are: + +| Template section | Main variables | What it does | Rust/verifier meaning | +| --- | --- | --- | --- | +| Load batching challenge | `y := mload(Y_MPTR)` | Reads the identity-batching challenge sampled by the main verifier. | `verify_algebraic_constraints` has already squeezed `y`. | +| Bind VM payload pointers | `q_const_mptr`, `q_program_mptr`, optional `q_tmp_mptr` | Points at quotient constants, quotient bytecode, and optional VM temporary scratch. | These are generated from `QuotientProgramBuild` and stored in the VK payload. | +| Initialize main fold state | `program.eval_numer_mptr`, `program.trace_id_mptr` | Zeros the Horner accumulator for fully evaluated identities and sets the trace id base. | This accumulator becomes `nu_y(x)` for the Rust `None` identity group. | +| Initialize selector buckets | `SELECTOR_ACC_MPTR`, `program.selector_power_mptr` | Zeros selector accumulators and precomputes the `y^k` powers needed by codegen-known selector gaps and tails. | Mirrors Rust's grouped simple-selector scalars in `compute_linearization_commitment`. | +| Direct inline prefix | `quotient_inline_computations` | Renders a short prefix of generated identity Yul before the VM loop. | Same identity stream, but cheaper than interpreter dispatch. | +| VM register setup | `q_pc`, `q_end`, `q_sp`, `q_top`, `q_has_top` | Initializes the stack interpreter over the VK-backed bytecode. | Implements lowered `partially_evaluate_identities` fragments. | +| Byte interpreter loop | `q_program_mptr..q_end` | Reads one opcode byte plus variable-width operands. Also supports dynamic run and limb opcodes. | Default compact encoding for the gas-capped verifier. | +| Native callback cases | generated callback blocks | VM markers reset the stack pointer and run generated Yul for permutation, lookup, or heavy gates. | Replaces regular identity fragments without changing order or y-batch positions. | +| Structured post-VM suffix | `quotient_post_vm_computations` | Emits regular identity families, currently trash, after the VM. | Keeps identity order while avoiding expensive interpreted tail work. | +| Final selector scaling | `program.selector_tail_updates` | Applies each selector bucket's codegen-known final y-power tail. | Converts gap-folded selector buckets into Rust's reverse-fold powers. | +| Linearization scalar store | `QUOTIENT_EVAL_MPTR` | Stores `-mload(eval_numer_mptr)`. | This is the expected scalar for the linearization query, not `h(x)`. | + +The legacy `None` branch is intentionally smaller conceptually: + +1. Load `delta`, `y`, and `q_trace_id`. +2. Render `quotient_eval_numer_computations` directly as Yul. +3. Store `-quotient_eval_numer` to `QUOTIENT_EVAL_MPTR`. + +That branch is used for older monolithic or experimental modes where the +compact VM is disabled. It follows the same Rust algebra, but the source is +large because every expression is unrolled into the template output. + +### Compact Quotient VM + +When compact quotient mode is active, most identities are encoded as q_program +bytecode stored in the VK payload. The Yul interpreter supports: + +- memory and constant loads; +- `add`, `mul`, and `neg` over `Fr`; +- fused add-mul forms for common expression shapes; +- reserved temp loads/stores for legacy quotient payloads; +- native callback markers for whole identity families; +- limb-aware opcodes for recurring 7-limb foreign-field expressions. + +The VM saves runtime bytecode and compile pressure. The tradeoff is gas: each +interpreted identity pays dispatch, stack/memory movement, and fold-state +loads/stores. Inline and native paths are larger but cheaper at runtime. + +The interpreter uses a cached-top stack: + +```text +q_pc current bytecode pointer +q_end end of bytecode +q_sp memory stack pointer for spilled operands +q_top cached top-of-stack field element +q_has_top whether q_top is live +``` + +Push-like cases spill the previous `q_top` to `mstore(q_sp, q_top)` and advance +`q_sp` when `q_has_top` is set. Binary stack cases decrement `q_sp`, combine +`mload(q_sp)` with `q_top`, and keep the result in `q_top`. Accumulator cases +mutate `q_top` directly. Fold cases consume `q_top` and advance the global +identity-batching state. + +### VM Switch Cases In The Template + +The Yul template has one byte-oriented switch loop. It decodes one opcode byte +followed by variable-width operands, including dynamic run opcodes and +limb-aware opcodes. + +The logical cases are: + +| Opcode | Case | Byte handling | Effect | +| --- | --- | --- | --- | +| `0x01` | `push_const` | next 2 bytes are const index | Spill old top if any, then load `const[const_idx]` into `q_top`. | +| `0x02` | `push_mem_literal` | next 4 bytes are pointer | Spill old top if any, then load `mload(ptr)` into `q_top`. | +| `0x03` | `push_mem_token` | next byte is token | Resolve token through the token switch and push `mload(ptr)`. Unknown token reverts. | +| `0x04` | `push_mem_token_offset` | next byte token, next 4 bytes offset | Resolve token, add byte offset, and push `mload(ptr + offset)`. | +| `0x05` | `push_mem_u16` | next 2 bytes are pointer | Short pointer load; push `mload(ptr)`. | +| `0x06` | `add` | no operand | Pop one spilled value and set `q_top = popped + q_top mod Fr`. | +| `0x07` | `mul` | no operand | Pop one spilled value and set `q_top = popped * q_top mod Fr`. | +| `0x08` | `neg` | no operand | Set `q_top = -q_top mod Fr`. | +| `0x09` | `push_const_u8` | next byte is const index | Short const load. | +| `0x0a` | `fold_main` | no operand | Trace `q_top`, clear it, and do `eval_numer = eval_numer * y + q_top`. | +| `0x0b` | `fold_selector` | next 3 bytes are selector index plus gap | Trace `q_top`, clear it, advance the global y position, multiply that selector bucket by `y^gap`, and add `q_top`. | +| `0x0c` | `add_const_u8` | next byte is const index | Mutate `q_top += const[const_idx]`. | +| `0x0d` | `mul_const_u8` | next byte is const index | Mutate `q_top *= const[const_idx]`. | +| `0x0e` | `add_const` | next 2 bytes are const index | Wider const add. | +| `0x0f` | `mul_const` | next 2 bytes are const index | Wider const multiply. | +| `0x10` | `add_mem_u16` | next 2 bytes are pointer | Mutate `q_top += mload(ptr)`. | +| `0x11` | `mul_mem_u16` | next 2 bytes are pointer | Mutate `q_top *= mload(ptr)`. | +| `0x12` | `add_mul_mem_mem_const_u8` | next `lhs,rhs,const` operands | Mutate `q_top += mload(lhs) * mload(rhs) * const[const_idx]`. | +| `0x13` | `add_mul_const_u8_mem_u16` | next `ptr,const` operands | Mutate `q_top += mload(ptr) * const[const_idx]`. | +| `0x14` | `add_mul_mem_mem` | next `lhs,rhs` operands | Mutate `q_top += mload(lhs) * mload(rhs)`. | +| `0x15` | `run_add_mul_mem_mem_const_u8` | dynamic run payload | Loop over repeated `0x12` payloads and accumulate each into `q_top`. | +| `0x16` | `run_add_mul_const_u8_mem_u16` | dynamic run payload | Loop over repeated `0x13` payloads and accumulate each into `q_top`. | +| `0x17` | `push_temp` | next 2 bytes are temp index | Push `mload(q_tmp_mptr + 32 * temp_idx)`. Current codegen does not emit it. | +| `0x18` | `store_temp` | next 2 bytes are temp index | Store `q_top` to `q_tmp_mptr + 32 * temp_idx`; `q_top` remains live. Current codegen does not emit it. | +| `0x19` | `native_permutation` | no operand | Reset VM top/stack and splice in the generated permutation identity callback. | +| `0x1a` | `reserved` | no case | Falls to `default { revert(0, 0) }` if present. | +| `0x1b` | `native_identity` | next 2 bytes are callback index | Reset VM top/stack and switch into one generated heavy-gate callback. | +| `0x1c` | `lin7` | seven limb terms | Push a 7-term table-backed linear combination. | +| `0x1d` | `bilin7_row` | lhs plus seven limb terms | Push one memory value times a 7-term weighted row. | +| `0x1e` | `bilin7_pairwise` | two bases plus 13 coefficients | Push a 7-by-7 weighted convolution over two contiguous limb vectors. | +| `0x1f` | `native_lookup` | no operand | Reset VM top/stack and splice in the generated LogUp lookup callback. | + +The token switch shared by `push_mem_token` and `push_mem_token_offset` maps: + +| Token | Template pointer | +| --- | --- | +| `0x01` | `L_0_MPTR` | +| `0x02` | `L_LAST_MPTR` | +| `0x03` | `L_BLIND_MPTR` | +| `0x04` | `BETA_MPTR` | +| `0x05` | `GAMMA_MPTR` | +| `0x06` | `X_MPTR` | +| `0x07` | `THETA_MPTR` | +| `0x08` | `TRASH_CHALLENGE_MPTR` | +| `0x09` | `INSTANCE_EVAL_MPTR` | + +Every unrecognized opcode, token, or native identity index goes to +`revert(0, 0)`. This is intentional: the VM program is generated and pinned in +the VK payload, so malformed bytecode means a generator or artifact mismatch, +not a recoverable proof failure. + +### Direct Inline Prefix + +The default IVC shape keeps a four-identity prefix as direct generated Yul before entering the VM. This gas-caps large IVC proofs because the earliest high-cost identities avoid interpreter dispatch while the remaining identities stay compact. + +### Native Callbacks + +Native callbacks are domain-shaped superinstructions: + +- native permutation preserves `permutation.rs` identity order but emits a + structured product loop instead of many interpreted operations; +- native lookup preserves LogUp order while sharing prefix/suffix scratch; +- native heavy identity callbacks are used for recognized expensive gate + identities. + +They are not semantic shortcuts. They still produce the same identity values and +advance the same `y` batch positions as the Rust verifier. + +### Simple-Selector Buckets + +Midfall does not require proof eval scalars for simple multiplicative selector +columns. Rust `compute_linearization_commitment` groups those identities by +selector commitment and puts the grouped scalar into the linearization MSM. + +The Yul block mirrors this with `SELECTOR_ACC_MPTR`. In forward VM order it uses +an inverse-`y` scale, then applies a final scale to match Rust's reverse fold. +The main verifier later expands each bucket into the fused final PCS MSM. + +### Structured Trash Tail + +The structured trash suffix can emit trash identities as direct structured Yul +after the VM. This trades some runtime bytes for lower gas on circuits where the +trash tail is expensive to interpret. The default gas-capped profile enables +this suffix. + +## PCS Yul Blocks + +`src/lowering/kzg/mod.rs` is the local equivalent of Midfall KZG `multi_prepare`. +It computes the query plan at lowering time, then emits fixed Yul blocks for the +generated circuit: + +1. Build the query list, skipping simple-selector fixed queries because the + linearization query carries those commitments. +2. Partition queries into point sets in the same way as + `poly/kzg/utils.rs::construct_intermediate_sets`. +3. Read prover `q_evals` and fold evaluations with `x1`. +4. Fold commitments with `x2`. +5. Interpolate the folded polynomial at `x3` to compute `f_eval`. +6. Fold the final commitment with `x4`. +7. Produce the two G1 inputs for the final KZG pairing. + +The linearization commitment is not materialized in production. Instead, its +quotient-limb terms and simple-selector terms are expanded directly into the +already-fused PCS MSM. Trace builds may materialize the point only for +comparison against the instrumented Rust verifier. + +## Optimisation Map + +| Optimisation | Where | Benefit | Tradeoff / invariant | +| --- | --- | --- | --- | +| Separate VK payload contract | `Halo2VerifyingKey.sol`, `VkPayloadLayout` | Moves large constants and commitments out of verifier runtime | Runtime is prefixed with `INVALID`; constructor and per-proof checks pin length/codehash | +| Split quotient evaluator | `Halo2QuotientEvaluator.sol` | Moves the largest scalar arithmetic body out of main verifier bytecode | Evaluator is correctness-critical and constructor-pinned | +| Compact quotient VM | `quotient/mod.rs`, `QuotientNumeratorBlock.yul` | Reduces Solidity/Yul bytecode and improves compile stability | Higher runtime gas for interpreted identities | +| Inline identity prefix | `quotient_program_plan` | Lowers gas for the first expensive identities | Increases verifier/evaluator runtime size | +| Native permutation/lookup callbacks | `generator.rs`, `QuotientNumeratorBlock.yul` | Avoids interpreter overhead on structured product loops | Callback scratch must be reserved by the memory planner | +| Structured trash suffix | default post-VM Yul block | Avoids interpreting trash identities | Increases generated Yul size | +| Quotient constants/program in VK payload | `artifact.rs`, `generate_vk` | Avoids verifier-side immediate constants | VK hash changes when the quotient program changes | +| Decoded eval spill buffer | `REVERSED_EVALS_MPTR` | Turns later eval references into cheap `mload`s | Proof scalar order must match protocol metadata exactly | +| Off-chain proof repacking | `quotient::RepackedProofLayoutPlan` and ABI docs | On-chain verifier consumes BE scalar words and EIP-2537 G1s directly | Calldata is Solidity-facing, not native Midnight proof bytes | +| Lagrange batch inversion | `batch_invert` helper | Computes all needed inverse denominators with one modexp | Scratch region must not overlap permanent memory | +| Simple-selector bucket grouping | `compute_linearization_commitment`, `SELECTOR_ACC_MPTR` | Removes simple selector proof evals and groups MSM terms | Selector identity order and final `y` scaling must match Rust | +| Fused linearization into PCS MSM | `pcs.rs` | Avoids a standalone production G1MSM for linearization | Trace builds may materialize it only for comparison | +| Point-set planning at lowering time | `pcs.rs::intermediate_sets` | Removes dynamic query sorting/grouping from Solidity | Generated verifier is circuit-specialized | +| Fewer point sets / dummy queries | `pcs.rs::compute_dummy_queries` | Can reduce KZG point-set work for selected profiles | Proof layout and transcript must include matching dummy evals | +| Truncated PCS challenges | `truncated-challenges` feature | Mirrors Midfall KZG challenge truncation where enabled | Only the specified challenges/powers are truncated | +| Accumulator pairing batch | `Halo2Verifier.sol` accumulator section | Combines public accumulator pairing with final KZG pairing | Batch randomizer is derived after all four G1 inputs are fixed | +| EIP-2537 gas caps and return-size checks | `TemplateConstants`, `layout::precompile::*_gas_cap` | Catches missing or incompatible precompiles without a runtime gas-table helper | Gas constants must match target fork assumptions | +| Gas checkpoints | `RenderDiagnostics { gas_checkpoints: true, .. }` | Gives stable section-level gas deltas | Not a `view` verifier; profiling only | + +## Trace Coverage + +Trace builds compare the Solidity verifier against the instrumented Rust +verifier for: + +- VK/domain values and Fiat-Shamir challenges; +- proof commitments and proof evaluations, including `q_evals`; +- reconstructed linearization expected scalar; +- selector fold buckets; +- PCS intermediate outputs and final pairing success. + +In split quotient mode, the evaluator is invoked via `STATICCALL`, so it cannot +emit internal `LOG` trace records. That means the monolithic path can compare +per-identity quotient trace ids, while the external IVC path compares the +inputs, returned scalar, selector buckets, and downstream PCS binding. + +## Reading The Generated Code + +When auditing a rendered verifier, read it in this order: + +1. Confirm the pinned VK/evaluator runtime lengths and codehashes. +2. Check the proof layout constants against `ProofCalldataLayout`. +3. Follow transcript absorption order through the challenge comments. +4. Confirm the evaluation buffer order matches `Protocol` metadata. +5. Read `QuotientNumeratorBlock.yul` as the lowering of + `partially_evaluate_identities`. +6. Read the linearization scalar block as the scalar half of + `compute_linearization_commitment`. +7. Read `pcs_computations` as the specialized KZG `multi_prepare` emitter. +8. Check final pairing and optional accumulator batching. + +The generated contracts are intentionally verbose around these boundaries. Most +bugs in this style of verifier are not arithmetic typos in a single expression; +they are order, layout, or challenge-binding mismatches between the Rust +verifier and the Solidity-facing proof format. diff --git a/proofs/solidity-verifier/docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md b/proofs/solidity-verifier/docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md new file mode 100644 index 000000000..634482f70 --- /dev/null +++ b/proofs/solidity-verifier/docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md @@ -0,0 +1,1711 @@ +# Halo2/Midnight Solidity Verifier Specification And Architecture + +This document is the implementation specification for this repository's +Halo2/Midnight Solidity verifier generator. It is intended to be complete +enough for a competent implementer to rebuild the verifier from scratch and +produce byte-compatible Solidity-facing proof verification semantics. + +The normative target is the current generator in: + +- `src/builder/` and `src/lowering/artifacts.rs` +- `src/lowering/protocol/mod.rs` +- `src/lowering/encoding/mod.rs` +- `src/lowering/kzg/mod.rs` +- `src/lowering/quotient_numerator/yul_emit.rs` +- `src/lowering/quotient_numerator/vm/mod.rs` +- `templates/contracts/Halo2Verifier.sol` +- `templates/contracts/Halo2VerifyingKey.sol` +- `templates/contracts/Halo2QuotientEvaluator.sol` +- `templates/partials/quotient_numerator/QuotientNumeratorBlock.yul` + +Existing focused notes remain useful, especially `docs/architecture/MEMORY_LAYOUT.md` and +`docs/reference/QUOTIENT_NUMERATOR_EVALUATOR.md`. For a template-by-template map from +Askama Solidity/Yul sections to the Midfall Rust verifier, see +`docs/reference/ASKAMA_TEMPLATE_RUST_MAPPING.md`. This document consolidates the full +verifier contract, proof format, transcript, algebra, KZG opening check, and +split-artifact architecture. + +## Midfall Comment Corpus And Porting Map + +`docs/reference/MIDFALL_PROOFS_COMMENT_CORPUS.md` preserves every Rust comment block from +`../midfall/proofs/src/**/*.rs`, grouped by upstream file and line span. Local +source and template comments intentionally adapt only the comments that map to +this generator: + +- Transcript comments from `transcript/mod.rs` and `transcript/implementors.rs` + map to `src/transcript.rs` and `templates/contracts/Halo2Verifier.sol`. +- PLONK verifier-flow comments from `plonk/verifier.rs` map to + `src/lowering/protocol/mod.rs`, verifier NatSpec, and proof-layout sections below. +- Identity and linearization comments from `plonk/mod.rs` and + `plonk/linearization/verifier.rs` map to `templates/partials/quotient_numerator/QuotientNumeratorBlock.yul` + and `docs/reference/QUOTIENT_NUMERATOR_EVALUATOR.md`. +- Askama template structure, generated Solidity/Yul sections, and optimization + tradeoffs are mapped in `docs/reference/ASKAMA_TEMPLATE_RUST_MAPPING.md`. +- KZG multi-open, dummy-query, point-set sorting, MSM, and pairing comments from + `poly/kzg/{mod.rs,msm.rs,utils.rs}` map to `src/lowering/kzg/mod.rs`. +- LogUp, permutation, and trash comments map to the quotient evaluator docs and + generated Yul comments; broader circuit/dev/floor-planning comments remain in + the corpus because they are not verifier behavior ported by this repository. + +## 1. Scope + +The generator emits Solidity verifiers for `midnight-proofs` / Midfall Halo2 +proofs using: + +- BLS12-381 KZG commitments. +- The Midnight/Midfall Keccak transcript. +- The Midnight PLONKish verifier shape, including gates, permutation, LogUp + lookups, trash arguments, simple selectors, and KZG multi-prepare. +- Solidity `^0.8.24` with Cancun `MCOPY`. +- EIP-2537 BLS12-381 precompiles: + - `0x0b`: G1ADD. + - `0x0c`: G1MSM. + - `0x0f`: PAIRING_CHECK. +- EVM modexp precompile `0x05` for scalar inversions. + +Supported execution target: an Ethereum-compatible Cancun-or-newer EVM with +`MCOPY` and Prague/EIP-2537 BLS12-381 precompiles at exactly the addresses +above, implementing the EIP-2537 input encodings, subgroup checks, return sizes, +and gas schedule. The repository CI/dev runner exercises this target through +Prague-spec `revm`; deployers on L2s, forks, or alt-EVMs must run the same +precompile conformance tests against their target chain before treating the +verifier as production-safe. + +The generated verifier is not a generic reusable verifier. It is +circuit-specialized. Circuit metadata, proof read order, quotient identity +program, VK payload, memory layout, and constant offsets are generated together +for one `VerifyingKey>`. + +### 1.1 Supported Protocol Shape + +The current generator intentionally accepts only this Midfall shape: + +- Exactly one proof. +- At least one advice column. +- Exactly two instance columns in the constraint system. +- Exactly one committed instance column. +- Exactly one non-committed public-input column. +- All instance queries must be at `Rotation::cur()`. +- KZG over BLS12-381. +- G1 proof elements are supplied to Solidity in EIP-2537 padded uncompressed + form, not in the native 48-byte compressed proof form. +- If a public KZG accumulator is enabled, it must use 7 limbs of 56 bits per + BLS12-381 base-field coordinate. + +`SolidityGenerator::try_new` rejects unsupported instance-column shapes and +rotated instance queries. `SolidityGenerator::new` panics on those same errors. + +### 1.2 Non-Goals + +The generated verifier does not: + +- Bind application semantics to public inputs. Application contracts must check + program identifiers, state roots, expected outputs, authorization, and domain + separation separately. +- Accept arbitrary VKs at runtime. +- Verify multiple proofs in one call. +- Parse native compressed Midnight proof bytes on chain. +- Decompress BLS12-381 points on chain. +- Provide a production audit guarantee. The repository README still marks the + verifier as unaudited. + +### 1.3 Genericity Boundary + +There are two different notions of "generic" in this codebase: + +1. A reusable verifier binary that accepts arbitrary verifying keys at runtime. +2. A generated verifier whose compact quotient VM can interpret a range of + generated quotient bytecode programs stored in the VK payload. + +The deployed artifact is not the first kind. Even in separate-VK mode, the +verifier pins `Halo2VerifyingKey` by exact runtime length and codehash and then +loads the pinned payload bytes with `extcodecopy`. The deployed VK runtime has a +one-byte `INVALID` prefix so direct calls cannot execute the data payload as EVM +code; the verifier copies from runtime byte `1`. The proof layout, query order, +evaluation counts, memory layout, selector buckets, lookup chunking, +permutation/trash shape, quotient frame length, and PCS point-set plan are all +generated from one `VerifyingKey>`. + +Before VM-case specialization, the quotient interpreter was closer to the +second kind: it rendered every opcode case supported by the selected physical +encoding. If a different pinned VK payload had contained a different valid +quotient bytecode program using another supported opcode, the interpreter +itself would probably not have been the blocker. The surrounding verifier still +would have been VK/generated-shape-specific for the reasons above. + +After VM-case specialization, the interpreter is also VK-specialized: codegen +scans the finalized quotient bytecode and renders only the opcode and memory +token cases that can occur in that pinned program. This shrinks deployed +runtime while preserving fail-closed behavior for malformed bytecode: any +unrendered opcode, reserved opcode, invalid token, or invalid native callback +index reaches `revert(0, 0)`. + +If a future deployment requirement is "one verifier binary for many VKs with +the same high-level CS parameters", this optimization must become optional or +be disabled for that profile. Such a generic profile would also need to revisit +VK/evaluator codehash pinning, proof-layout constants, native callbacks, and +any generated Yul that is derived from a single identity stream rather than +from runtime VK data. + +## 2. Terminology + +This repository follows `midnight_curves` naming, where `Fq` is used as the +native scalar field type for the BLS12-381 proof system. In this document: + +- `Fr` means the BLS12-381 scalar field used for proof scalars and all PLONK + algebra. +- `r` means the `Fr` modulus: + `0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001`. +- `Fp` means the BLS12-381 base field used for G1/G2 coordinates. +- `p` means the BLS12-381 base-field modulus. +- `n = 2^k` means the evaluation-domain size. +- `omega` means the `n`-th root of unity from the VK domain. +- `rotation_last = -(blinding_factors + 1)`. +- `G1` points use EIP-2537 padded uncompressed layout: + `x_hi || x_lo || y_hi || y_lo`, four 32-byte words. +- `G2` points use EIP-2537 padded Fp2 layout, eight 32-byte words: + `x.c0_hi, x.c0_lo, x.c1_hi, x.c1_lo, y.c0_hi, y.c0_lo, y.c1_hi, + y.c1_lo`. + +All Solidity field arithmetic over `Fr` is performed with `addmod` and +`mulmod` modulo `r`. Scalar inversions use modexp `x^(r - 2) mod r`. + +## 3. Artifact Architecture + +The generator can emit three contracts: + +- `Halo2Verifier`: the proof verifier and public ABI. +- `Halo2VerifyingKey`: a data contract whose runtime bytecode is + `INVALID || VK payload`. +- `Halo2QuotientEvaluator`: an optional split helper for quotient numerator + reconstruction. + +### 3.1 Embedded Mode + +`SolidityGenerator::render(RenderOptions { vk: RenderVk::Embedded, .. })` +emits one `Halo2Verifier` contract with the VK payload embedded as `mstore` +constants. There is no external VK dependency. + +### 3.2 Separate VK Mode + +`SolidityGenerator::render(RenderOptions { vk: RenderVk::Separate, .. })` +emits: + +- `Halo2Verifier`. +- `Halo2VerifyingKey`. + +The VK contract constructor writes an unconditional `INVALID` byte followed by +the payload into memory and returns that prefixed runtime bytecode. The +verifier: + +1. Stores the expected VK payload length. +2. Stores the expected VK runtime length (`payload length + 1`). +3. Stores the expected VK runtime `keccak256` codehash over the prefixed + runtime. +4. Accepts the VK address in the constructor. +5. Checks length and codehash in the constructor and again per proof. +6. Loads the VK payload using `extcodecopy(vk, VK_MPTR, 1, vk_payload_len)`. + +The separate VK is a code-size split, not a new trust boundary. The generated +verifier accepts exactly the pinned runtime bytes. Because some deployments may +target forks with weaker code-immutability assumptions, `verifyProof` re-checks +the pinned length/codehash before copying payload bytes. + +### 3.3 Split Quotient Mode + +For large circuits, quotient numerator reconstruction is the largest generated +arithmetic block. The default separated VK render keeps that block inside +`Halo2Verifier` when the merged verifier remains below EIP-170. The generator +can still split it into `Halo2QuotientEvaluator` for larger circuits or +bytecode experiments. + +Split quotient flow: + +1. Render the quotient evaluator with `render_quotient_evaluator(diagnostics)`. +2. Compile and deploy it. +3. Compute its deployed runtime length and codehash. +4. Render the verifier and VK with + `RenderQuotient::ExternalPinned { runtime_len, codehash }`. +5. Deploy the VK. +6. Deploy the verifier with the VK address and quotient evaluator address. + +The verifier pins the quotient evaluator exactly like the VK. The quotient +evaluator is correctness-critical; an unpinned evaluator could change the +statement being checked. + +The quotient evaluator has only a fallback entry point. It receives a raw +memory frame, not normal ABI arguments: + +```text +calldata[0..QUOTIENT_FRAME_LEN) + == verifier_memory[QUOTIENT_FRAME_BASE..QUOTIENT_FRAME_BASE + QUOTIENT_FRAME_LEN) +``` + +It returns: + +```text +word 0: QUOTIENT_MAGIC = 0x51554556414c0001 +word 1: linearization_expected_eval +word 2..: one selector accumulator scalar per simple selector +``` + +The verifier checks the return size and magic before using the output. + +## 4. Public Solidity ABI + +The verifier exposes: + +```solidity +function verifyProof(bytes calldata proof, uint256[] calldata instances) + external + view + returns (bool); +``` + +Trace builds, and artifacts rendered through the explicit +`RenderDiagnostics { gas_checkpoints: true, .. }` profiling renders drop `view` because they emit +logs. Enabling the `solidity-gas-checkpoints` Cargo feature alone does not make +default renders non-view; checkpoint logging in default renders also requires +`solidity-trace`. + +The function selector is: + +```text +verifyProof(bytes,uint256[]) = 0x1e8e1e13 +``` + +The verifier is success-or-revert: + +- Valid proof: returns `true`. +- Malformed calldata, invalid scalar, invalid point, wrong dependency code, + failed precompile, or failed pairing: reverts, usually with empty data. + +### 4.1 ABI Layout Requirements + +The verifier hand-parses calldata and requires the canonical two-argument ABI +layout: + +```text +0x00..0x03: function selector +0x04..0x23: proof offset, must be 0x40 +0x24..0x43: instances offset, must be 0x40 + 0x20 + proof_len +0x44..0x63: proof length, must equal generated proof_len +0x64.. : proof bytes +... : instances length word +... : instances words +``` + +The concrete byte offsets are generated by `ProofCalldataLayout` from the +typed protocol plan. For the canonical ABI prefix they are: + +```text +PROOF_LEN_CPTR = 0x44 +PROOF_CPTR = 0x64 +NUM_INSTANCE_CPTR = 0x64 + proof_len +INSTANCE_CPTR = NUM_INSTANCE_CPTR + 0x20 +``` + +`calldatasize()` must equal: + +```text +INSTANCE_CPTR + 0x20 * num_instances +``` + +The `instances` length must equal `VK.num_instances`. Every instance word is a +canonical big-endian `Fr` element and must be `< r`. + +## 5. Solidity-Facing Proof Encoding + +Native `midnight-proofs` proof bytes and Solidity proof bytes are different. + +Native proof: + +- G1 elements are 48-byte compressed BLS12-381 encodings. +- Scalar elements are 32-byte canonical little-endian field representations. + +Solidity proof: + +- Every G1 is decompressed off chain and encoded as 128 bytes: + `x_hi || x_lo || y_hi || y_lo`. +- Every scalar is rewritten to a 32-byte big-endian EVM word. + +`SolidityGenerator::repack_proof` performs this transformation and +must be used by callers that start from native proof bytes. + +### 5.1 G1 Padded Encoding + +For an affine non-identity G1 point: + +```text +x_be = 48-byte big-endian Fp x coordinate +y_be = 48-byte big-endian Fp y coordinate + +x_hi = 16 zero bytes || x_be[0..16] +x_lo = x_be[16..48] +y_hi = 16 zero bytes || y_be[0..16] +y_lo = y_be[16..48] +``` + +The identity is encoded as 128 zero bytes. + +The verifier rejects: + +- Non-zero top 16 bytes in `x_hi` or `y_hi`. +- Coordinates `>= p`. + +Curve and subgroup validation are delegated to EIP-2537 precompiles when the +absorbed point is later consumed by G1MSM, G1ADD, or pairing. The protocol plan +rejects generated shapes where an absorbed proof commitment is never consumed +by a validating precompile path. + +### 5.2 Proof Element Order + +The Solidity proof byte stream is: + +```text +1. Advice commitments, grouped by user phase. +2. Lookup multiplicity commitments, one per lookup. +3. Permutation product commitments, one per permutation set. +4. For each lookup: + - lookup helper commitments, one per lookup chunk; + - lookup accumulator commitment. +5. Trash commitments, one per trash argument. +6. Quotient commitment(s): one G1 point with `outer-single-h-commitment`, + otherwise `degree - 1` G1 points. +7. Main evaluation scalars. +8. `f_com` G1. +9. `q_eval` scalars, one per PCS point set. +10. `pi` G1. +``` + +The corresponding transcript challenge squeezes occur between these reads, as +specified in section 7. `ProofCalldataLayout` is the Rust source of truth for +the byte range of each section, and the Solidity parser/repacker consume those +generated ranges rather than recomputing section lengths independently. + +### 5.3 Main Evaluation Scalar Order + +After the `x` challenge, the verifier reads `num_evals` scalar words in this +exact order: + +1. Committed instance query evaluations for instance queries whose column is + less than `num_committed_instances`. +2. Advice query evaluations, in `ConstraintSystem::advice_queries()` order. +3. Fixed query evaluations for non-simple-selector fixed columns, in + `ConstraintSystem::fixed_queries()` order. +4. Permutation common evaluations, one for each permutation column. +5. Permutation product evaluations: + - `z_cur` and `z_next` for every permutation set; + - `z_last` for every set except the final set. +6. For each lookup: + - lookup multiplicity evaluation; + - lookup helper evaluations, one per chunk; + - lookup accumulator `z` at rotation 0; + - lookup accumulator `z_next` at rotation 1. +7. Trash evaluation scalars, one per trash argument. +8. Dummy eval scalars if the `outer-fewer-point-sets` feature is enabled. + +Simple selector fixed-column evaluations are not read from the proof. They are +represented as selector accumulator scalars and fixed commitments in the +linearization query. + +### 5.4 Proof Length + +Let: + +```text +total_advices = sum advice commitments across user phases +lookup_helper_chunks_total = sum lookup chunk counts +non_quotient_g1s = + total_advices + + num_lookups // multiplicity commitments + + num_permutation_zs // permutation products + + lookup_helper_chunks_total + + num_lookups // lookup accumulator commitments + + num_trashcans + +num_quotients = 1 when outer-single-h-commitment is enabled, + otherwise cs.degree() - 1 +batch_g1s = 2 // f_com, pi +num_point_sets = PCS intermediate point-set count +``` + +`ProofCalldataLayout` computes the final value. For the current compatible +layout this is: + +```text +proof_len = + (non_quotient_g1s + num_quotients + batch_g1s) * 128 + + (num_evals + num_point_sets) * 32 +``` + +The verifier checks the proof bytes length exactly. + +### 5.5 Outer Single-H Decider Layout + +The `outer-single-h-commitment` feature mirrors Midfall's +`midnight-proofs/single-h-commitment` for the final Solidity-facing proof only. +It does not enable `single-h-commitment` in `midnight-circuits`, +`midnight-zk-stdlib`, or `midnight-aggregation`, so recursive proofs verified +inside the decider circuit remain on the multi-limb quotient layout. + +Operationally, the IVC bench runs in two phases: + +1. compile without `outer-single-h-commitment` and write a multi-limb leaf + bundle containing the two leaf states, proof bytes, and final collapsed + accumulator; +2. compile with `outer-single-h-commitment`, load that bundle, and prove the + final Keccak decider proof with one quotient commitment. + +The single-H proof requires an extended monomial SRS for the decider quotient +polynomial. For the current decider, `DECIDER_K = 20` and `cs_degree = 5`, so: + +```text +extended_k = 20 + ceil_log2(5 - 1) = 22 +``` + +The default bench therefore needs `midnight-srs-2p19`, `midnight-srs-2p20`, and +`midnight-srs-2p22`. Passing `--skip-srs-download` fails closed if `2p22` is +not already present. + +For the current VK, single-H changes the final PCS linearization contribution +from `4` quotient commitment terms to `1`. The fused final MSM therefore drops +from `78` to `75` terms. The EIP-2537 G1MSM gas table makes this a marginal +runtime change. In the current profiled IVC command, PCS block 5 drops by +`17,912` gas and total transaction gas drops by `24,571`; the quotient +numerator reconstruction does not change. + +## 6. Verifying Key Payload + +The VK payload is a sequence of 32-byte big-endian words. In separate mode it +starts at runtime byte `1` of `Halo2VerifyingKey`; runtime byte `0` is +`INVALID` and is included in the pinned runtime length/codehash. + +Header layout is generated by `VkHeaderLayout` and written through +`VkHeaderBuilder` so each field is placed by descriptor rather than append +order: + +```text +word 0: vk_digest +word 1: num_instances +word 2: k +word 3: n_inv +word 4: omega +word 5: omega_inv +word 6: omega_inv_to_l = omega_inv ^ abs(rotation_last) +word 7: has_accumulator, 0 or 1 +word 8: acc_offset +word 9: num_acc_limbs +word 10: num_acc_limb_bits +word 11..14: G1_BASE +word 15..22: G2_BASE +word 23..30: NEG_S_G2_BASE = -[s]G2 +``` + +All scalar words are `Fr` values in big-endian EVM word form. `G1_BASE`, +`G2_BASE`, and `NEG_S_G2_BASE` use EIP-2537 padded encodings. + +After the header. The quotient sections may have zero length in experimental +non-VM quotient modes, but are present in the default compact VM mode: + +```text +word 31..: quotient constant pool +then: quotient bytecode program, padded to whole words +then: fixed commitments, 4 words each +then: permutation commitments, 4 words each +``` + +The exact offsets for the quotient constant pool and quotient program are +computed by `VkPayloadLayout`; the verifier uses generated constants derived +from that layout. + +The VK codehash pins: + +- Header constants. +- SRS-derived G2 data. +- Fixed commitments. +- Permutation commitments. +- Quotient VM constants. +- Quotient VM bytecode. + +## 7. Fiat-Shamir Transcript + +The generated verifier implements the Midfall Keccak transcript. + +### 7.1 Transcript State + +The transcript state is a byte buffer. + +```text +init: + buffer = empty + +common(bytes): + buffer = buffer || bytes + +squeeze(): + digest = keccak256(buffer) + buffer = digest + return uint256_be(digest) mod r +``` + +Scalar inputs are canonical 32-byte big-endian words. G1 inputs are the +128-byte EIP-2537 padded uncompressed form. + +### 7.2 Absorb And Squeeze Schedule + +The verifier must execute this schedule exactly: + +1. Initialize empty transcript. +2. Absorb `vk_digest`. +3. Absorb `committed_pi = G1 identity` as 128 zero bytes. This mirrors the + current `midnight-proofs` committed-instance feature shape used by the + generator. +4. Absorb `num_instances`. +5. Absorb each public `instances` scalar in calldata order. +6. For each user phase: + - absorb that phase's advice commitments; + - squeeze that phase's user challenges into `CHALLENGE_MPTR`. +7. Squeeze `theta`. +8. Absorb lookup multiplicity commitments, if any. +9. Squeeze `beta`. +10. Squeeze `gamma`. +11. Absorb permutation product commitments, if any. +12. Absorb lookup helper and lookup accumulator commitments, if any. +13. Squeeze `trash_challenge`. This is unconditional. +14. Absorb trash commitments, if any. +15. Squeeze `y`. +16. Absorb quotient commitment(s). +17. Squeeze `x`. +18. Absorb all main evaluation scalars, including dummy evals if present. +19. Squeeze `x1`. +20. Squeeze `x2`. +21. Absorb `f_com`. +22. Squeeze `x3`. +23. If `truncated-challenges` is enabled, replace `x3` with its low 128 bits. +24. Absorb all `q_eval` scalars, one per PCS point set. +25. Squeeze `x4`. +26. Absorb `pi`. + +The parser must consume exactly `proof_len` proof bytes after absorbing `pi`. + +### 7.3 Truncated Challenges + +When the crate feature `truncated-challenges` is enabled, the Solidity verifier +must mirror `midnight-proofs`: + +- `x3` is masked to the lower 128 bits immediately after squeeze. +- `x1` powers used in PCS are emitted as `truncate(x1^i)`, while the internal + power accumulator remains full precision. +- `x4` powers are emitted as `truncate(x4^i)`, while the internal power + accumulator remains full precision. + +The mask is: + +```text +0xffffffffffffffffffffffffffffffff +``` + +Feature settings must match the prover. A mismatch produces invalid pairings. + +## 8. Protocol Plan + +`ProtocolPlan::from_constraint_system` is the source of truth for proof reads, +PCS queries, common polynomial needs, and quotient identity counts. + +### 8.1 Derived Counts + +From the constraint system: + +```text +num_fixeds = cs.num_fixed_columns() +permutation_columns = cs.permutation().get_columns() +permutation_chunk_len = cs.degree() - 2 +num_permutation_zs = ceil(len(permutation_columns) / permutation_chunk_len) +lookup_chunks[i] = cs.lookups()[i].chunk_by_degree(cs.degree()).num_chunks() +num_lookups = len(lookup_chunks) +num_trashcans = len(cs.trashcans()) +num_quotients = 1 when outer-single-h-commitment is enabled, + otherwise cs.degree() - 1 +num_simple_selectors = cs.num_simple_selectors() +simple_selector_cols = fixed columns where cs.has_simple_selector_col(idx) +rotation_last = -(cs.blinding_factors() + 1) +``` + +Advice and challenge columns are remapped by phase. Commitments are still read +in phase order, but their memory column indices are compact phase-local +indices, matching the prover's read order. + +### 8.2 Commitment Read Plan + +The commitment plan is: + +```text +advice commitments in phase-remapped order +lookup multiplicity commitments +permutation product commitments +for each lookup: + lookup helper commitments + lookup accumulator commitment +trash commitments +quotient commitment(s) +``` + +Every absorbed proof commitment must later be consumed by PCS or another +EIP-2537 validating path. + +### 8.3 PCS Query Plan + +The PCS query list is: + +```text +advice queries +committed instance queries +permutation Z queries +lookup multiplicity queries +lookup helper queries +lookup accumulator queries +trash queries +fixed queries, excluding simple selector columns +permutation common queries +linearization query +``` + +The linearization query is always last. + +For `PermutationZ`: + +```text +Cur -> rotation 0 +Next -> rotation 1 +Last -> rotation_last +``` + +For lookup accumulators: + +```text +z -> rotation 0 +z_next -> rotation 1 +``` + +All other special argument queries are at rotation 0 unless explicitly stated. + +### 8.4 Common Polynomials + +The verifier must make available: + +- Every rotation point `x * omega^rot` used by PCS queries. +- `L_0(x)`, `L_last(x)`, and `L_blind(x)` when permutation or lookup arguments + require them. +- Rotation 0 when trash arguments require it. + +The generated Solidity computes these after `x` is sampled. + +## 9. Public Instance Evaluation + +Committed instance column evaluations are read from the proof and included in +the PCS query list. + +The non-committed public input column is evaluated locally from `instances`. +The verifier computes Lagrange values at `x` and forms: + +```text +instance_eval = sum_i L_i(x) * instances[i] +``` + +The same Lagrange block also computes: + +```text +x_n +x_n_minus_1_inv +L_last(x) +L_blind(x) +L_0(x) +``` + +Implementation details: + +1. Compute `x_n = x^(2^k)` by repeated squaring `k` times. +2. Build denominator inputs `x - omega^j` for the required negative rotations + and public-instance positions. +3. Append `x_n - 1`. +4. Batch invert all denominators. +5. Use the common factor `(x_n - 1) * n_inv`. +6. Store the generated Lagrange values and `instance_eval`. + +The batch inversion helper must fail if any inverted scalar is zero. + +## 10. Quotient Numerator And Linearization Scalar + +The verifier reconstructs the same identity evaluations as +`midnight-proofs::plonk::partially_evaluate_identities`, in this order: + +1. Custom gate identities. +2. Permutation identities. +3. Lookup identities. +4. Trash identities. + +The output is not a prover-supplied quotient evaluation. The verifier computes +the y-batched numerator scalar: + +```text +nu_y(x) = y^(m-1) * e_0(x) + y^(m-2) * e_1(x) + ... + e_(m-1)(x) +``` + +where `e_i` are the identities in forward Rust order. + +The verifier stores: + +```text +linearization_expected_eval = -nu_y(x) mod r +``` + +This value is the claimed opening scalar for the generated linearization +commitment in the final PCS query. + +### 10.1 Gate Identities + +Gate expressions are lowered from `midnight_proofs::plonk::Expression`. + +Expression leaves map as follows: + +- Constants: literal `Fr` constants. +- Advice queries: proof eval words by `(column, rotation)`. +- Fixed queries: proof eval words by `(column, rotation)`, except simple + selectors. +- Simple selectors: handled as selector targets, not proof eval scalars. +- Instance queries: + - committed instance columns use proof evals; + - non-committed public input column uses `INSTANCE_EVAL_MPTR`. +- Challenge queries: sampled user challenges. + +Expression nodes map to `addmod`, `mulmod`, and field negation. + +If a gate identity is gated by a simple selector, its scalar contribution goes +into the selector bucket for that fixed column rather than into +`quotient_eval_numer`. + +### 10.2 Permutation Identities + +The verifier emits the same permutation constraints as the Midfall verifier: + +- First-set boundary: + `L_0(x) * (1 - z_0(x))`. +- Last-set boolean boundary: + `L_last(x) * (z_l(x)^2 - z_l(x))`. +- Cross-set continuity for every set after the first: + `L_0(x) * (z_i(x) - z_(i-1)(omega^last * x))`. +- Active-row product check for every set: + +```text +(1 - (L_last(x) + L_blind(x))) * +( + z_i(omega*x) * product(p(x) + beta*s_i(x) + gamma) + - z_i(x) * product(p(x) + delta^i*beta*x + gamma) +) +``` + +`delta` is the BLS12-381 `Fr::DELTA` constant used by Midfall: + +```text +3793952369011177517951424454785176000433849974408744014172535497121832470999 +``` + +### 10.3 Lookup Identities + +For LogUp lookups, multi-expression inputs are compressed with `theta` in +Horner form: + +```text +compressed = (((expr_0) * theta + expr_1) * theta + ...) +``` + +The verifier emits: + +- Boundary: `(L_0(x) + L_last(x)) * Z(x)`. +- Helper constraints per lookup input chunk: + +```text +h_i(x) * product_j(f_j(x) + beta) + - sum_j product_{k != j}(f_k(x) + beta) +``` + +- Accumulator constraint on active rows: + +```text +( + Z(omega*x) + - Z(x) + - selector(x) * sum_i h_i(x) +) * (t(x) + beta) ++ m(x) +``` + +then multiplied by: + +```text +1 - (L_last(x) + L_blind(x)) +``` + +### 10.4 Trash Identities + +Each trash argument compresses its constraint expressions with +`trash_challenge` in Horner form, then subtracts the inactive-row trash term: + +```text +compressed - (1 - q(x)) * trash_eval +``` + +### 10.5 Y-Batching + +Rust folds in reverse with powers of `y`. Solidity scans forward using Horner: + +```text +acc = 0 +for identity e_i in forward order: + acc = acc * y + e_i +``` + +This produces the same y powers as Rust's reverse fold. + +### 10.6 Simple Selector Buckets + +Simple-selector identities must retain their y-batch position while grouping by +selector commitment. + +The compact VM keeps: + +```text +selector_bucket[j] +y_power[k] = y^k +``` + +Codegen knows the global positions of selector identities. For a +selector-targeted identity with scalar `e`, it emits the gap since the previous +identity for the same selector: + +```text +selector_bucket[j] = selector_bucket[j] * y_power[gap] + e +``` + +After all identities it applies the final tail for each selector: + +```text +selector_bucket[j] *= y_power[tail[j]] +``` + +This matches Rust's grouped selector accumulators. + +### 10.7 Linearization Commitment Side + +The commitment for the final linearization query is: + +```text +(1 - x^n) * sum_i x_split^i * Q_i + + sum_j selector_bucket[j] * S_j +``` + +where: + +```text +x_split = x^(n - 1) +Q_i = quotient commitment i +S_j = simple selector fixed commitment j +``` + +In the outer single-H layout the sum has one `Q_0` term, so the quotient-side +scalar is exactly `1 - x^n`. + +The scalar side is `-nu_y(x)`. The verifier does not compute or trust +`h(x) = nu_y(x) / (x^n - 1)`. + +## 11. Compact Quotient VM + +The default generator stores most quotient identity arithmetic as data in the +VK payload and interprets it with a small Yul stack VM. + +A reimplementation may emit straight-line code instead, but it must produce +the same identity values in the same order and the same selector/main folds. +To reproduce this repository's split artifact shape, implement the VM below. +The generator decodes the finalized byte-oriented bytecode after run compaction +and rejects unknown opcodes, truncated operands, unknown memory tokens, stack +underflow, native-callback stack leaks, and non-empty identity boundaries before +pinning the program into the VK payload. + +### 11.1 VM State + +The VM has: + +- `q_const_mptr`: base of the quotient constant pool in the copied VK payload. +- `q_program_mptr`: base of quotient bytecode in the copied VK payload. +- `q_pc`: bytecode cursor. +- `q_end`: bytecode end. +- `q_sp`: memory stack pointer. +- `q_top`: cached top-of-stack. +- `q_has_top`: whether `q_top` is live. +- Optional VM temp words at `q_tmp_mptr` for legacy temp payloads. Current + codegen emits no temp opcodes. + +Push semantics: + +- If `q_has_top` is true, spill `q_top` to `mstore(q_sp, q_top)` and advance + `q_sp += 0x20`. +- Load the new value into `q_top`. +- Set `q_has_top = 1`. + +Binary semantics: + +- Decrement `q_sp` by `0x20`. +- Combine `mload(q_sp)` with `q_top`. +- Store the result in `q_top`. + +### 11.2 Memory Tokens + +Token operands map to generated memory pointers: + +```text +0x01: L_0_MPTR +0x02: L_LAST_MPTR +0x03: L_BLIND_MPTR +0x04: BETA_MPTR +0x05: GAMMA_MPTR +0x06: X_MPTR +0x07: THETA_MPTR +0x08: TRASH_CHALLENGE_MPTR +0x09: INSTANCE_EVAL_MPTR +``` + +Token-offset instructions add a byte offset to the token pointer. + +### 11.3 Byte-Oriented Opcode Table + +The default physical encoding is byte-oriented. Multi-byte numeric operands are +big-endian. + +```text +0x01 u16 const_idx: + push mload(q_const_mptr + 32 * const_idx) + +0x02 u32 ptr: + push mload(ptr) + +0x03 u8 token: + push mload(ptr_for_token(token)) + +0x04 u8 token, u32 offset: + push mload(ptr_for_token(token) + offset) + +0x05 u16 ptr: + push mload(ptr) + +0x06: + q_top = stack_pop() + q_top mod r + +0x07: + q_top = stack_pop() * q_top mod r + +0x08: + q_top = -q_top mod r + +0x09 u8 const_idx: + push mload(q_const_mptr + 32 * const_idx) + +0x0a: + fold main identity with q_eval = q_top + +0x0b u8 selector_idx, u16 gap: + advance that selector bucket by gap and fold q_eval = q_top + +0x0c u8 const_idx: + q_top += const[const_idx] + +0x0d u8 const_idx: + q_top *= const[const_idx] + +0x0e u16 const_idx: + q_top += const[const_idx] + +0x0f u16 const_idx: + q_top *= const[const_idx] + +0x10 u16 ptr: + q_top += mload(ptr) + +0x11 u16 ptr: + q_top *= mload(ptr) + +0x12 u16 lhs, u16 rhs, u8 const_idx: + q_top += mload(lhs) * mload(rhs) * const[const_idx] + +0x13 u16 ptr, u8 const_idx: + q_top += mload(ptr) * const[const_idx] + +0x14 u16 lhs, u16 rhs: + q_top += mload(lhs) * mload(rhs) + +0x15 u16 count, repeated count times {u16 lhs, u16 rhs, u8 const_idx}: + run form of 0x12 + +0x16 u16 count, repeated count times {u16 ptr, u8 const_idx}: + run form of 0x13 + +0x17 u16 temp_idx: + push mload(q_tmp_mptr + 32 * temp_idx) + +0x18 u16 temp_idx: + mstore(q_tmp_mptr + 32 * temp_idx, q_top) + +0x19: + native permutation callback + +0x1a: + reserved / unassigned + +0x1b u16 native_idx: + native heavy-gate callback + +0x1c repeated 7 times {u8 const_idx, u16 ptr}: + LIN7 = sum_i const[const_idx_i] * mload(ptr_i) + +0x1d u16 lhs, repeated 7 times {u8 const_idx, u16 rhs}: + BILIN7_ROW = mload(lhs) * sum_i const[i] * mload(rhs_i) + +0x1e u16 lhs_base, u16 rhs_base, 13 bytes const_idx[0..12]: + BILIN7_PAIRWISE = + sum_{i=0..6,j=0..6} + const[const_idx[i+j]] + * mload(lhs_base + 32*i) + * mload(rhs_base + 32*j) + +0x1f: + native lookup callback + +0x20: + q_top = q_top^5 + +0x21 flags, optional cond/constant, count header, fused 7-limb blocks: + push one fused affine 7-limb identity value +``` + +The full interpreter case reference, implementation notes, and audit checklist +for this VM live in `docs/reference/QUOTIENT_NUMERATOR_EVALUATOR.md`. In particular, +native callbacks may use scratch memory that is larger than the interpreter's +operand stack. Memory planning must reserve the maximum of the VM stack depth +and any enabled native callback scratch requirement. + +## 12. KZG Multi-Prepare PCS Check + +The PCS emitter mirrors `midnight-proofs` KZG `multi_prepare`. + +### 12.1 Raw Query List + +Build queries from the protocol plan. Each query is: + +```text +(commitment, rotation, claimed_eval) +``` + +The final query is the linearization query: + +```text +(linearization_commitment, 0, linearization_expected_eval) +``` + +### 12.2 Fewer Point Sets + +If `outer-fewer-point-sets` is enabled: + +1. Group raw queries by commitment. +2. Build the union of all non-singleton point sets. +3. For each commitment group missing a point from that union, append a dummy + query using that commitment and a new dummy eval scalar. +4. Dummy eval scalars are appended to the main evaluation block before `f_com`. + +The dummy-query order is deterministic: group insertion order, then union point +in insertion order. + +### 12.3 Intermediate Sets + +Construct intermediate sets: + +1. Deduplicate commitments by identity. +2. For each commitment, record all queried rotation points and aligned evals. +3. Bucket commitments by their set of rotation points. +4. Convert point-index sets back to rotation lists. +5. Sort point sets by ascending cardinality, with original set index as + tiebreaker. + +`num_point_sets` is the number of sorted sets. The proof contains exactly one +`q_eval` scalar for each set. + +### 12.4 Rotation Points + +For every distinct rotation in every point set, compute: + +```text +point(rot) = x * omega^rot +``` + +Positive rotations walk by multiplying by `omega`. Negative rotations walk by +multiplying by `omega_inv`. + +### 12.5 x1 Powers + +For the maximum number of commitments in any point set, compute: + +```text +x1_powers[i] = x1^i +``` + +If `truncated-challenges` is enabled, store `low128(x1^i)` while keeping the +internal accumulator full precision. + +### 12.6 q_eval_set + +For each point set `s`, and each rotation position `k` inside that set: + +```text +q_eval_set[s][k] = + sum_i x1_powers[i] * eval(commitment_i, rotation_k) +``` + +The storage order is flattened by set order: + +```text +Q_EVAL_SET_MPTR + 32 * (sum_{t::as_public_input`: +`lhs point, lhs scalar, rhs point, rhs scalar`, with an optional fixed-base +scalar tail. Moonlight wrap proofs expose an already-collapsed point pair +instead. For that ABI, use: + +```rust +let generator = SolidityGenerator::new( + params, + vk, + GeneratorConfig::new(num_instances, num_committed_instances) + .with_accumulator(AccumulatorEncoding::point_pair(offset, 7, 56)), +); +``` + +The point-pair layout is `lhs point, rhs point`; the generated verifier uses +implicit unit scalars for both sides and rejects fixed-base scalar tails. + +Use `SolidityGenerator::try_new` when the public-input layout comes from +caller-controlled metadata and should return a typed `GeneratorError` instead +of panicking. Enabling the accumulator in `GeneratorConfig` writes the expected +accumulator metadata into the generated VK payload: + +```text +has_accumulator = 1 +acc_offset = offset +num_acc_limbs = 7 +num_acc_limb_bits = 56 +``` + +The Solidity verifier checks those VK header words against the generator's +compiled-in expectations before it decodes public inputs. A stale VK or a +verifier rendered with the wrong accumulator metadata therefore reverts before +any accumulator point reconstruction. + +The current Solidity path requires: + +```text +num_limbs = 7 +num_limb_bits = 56 +``` + +### 13.1 Public Input Layout + +The accumulator is not passed through a separate ABI argument. It is a public +input tail inside the normal non-committed `instances` vector, beginning at +`instances[offset]`. The circuit must constrain that public tail to equal the +carried accumulator it recomputes internally. The IVC decider does this by +formatting: + +```text +[leaf public state words..., fully collapsed final accumulator public input] +``` + +and passing the starting index of the accumulator tail as `offset`. + +At `instances[offset]`, the decoded schema is: + +```text +lhs point coordinates +lhs scalar +rhs point coordinates +rhs scalar +optional RHS fixed-base scalar tail +``` + +The verifier computes the expected total public-input width: + +```text +expected_words = + offset + + lhs point words + + lhs scalar word + + rhs point words + + rhs scalar word + + fixed-base scalar tail words +``` + +and requires `num_instances == expected_words`. This makes the accumulator a +checked tail convention, not an unchecked side channel: extra words after the +accumulator and missing accumulator words both cause verification to revert. + +With 7 limbs and 56-bit limbs: + +```text +limbs_per_word = 4 +coord_words = ceil(7 / 4) = 2 +point_words = 2 * coord_words = 4 +``` + +So the collapsed no-tail layout is: + +```text +lhs_x: 2 words +lhs_y: 2 words +lhs_scalar: 1 word +rhs_x: 2 words +rhs_y: 2 words +rhs_scalar: 1 word +``` + +If a fixed-base scalar tail exists, it is consumed as RHS MSM scalars for: + +- `-G`, represented by `G1_BASE` with scalar negated modulo `r`; +- fixed commitments; +- permutation commitments. + +The tail bases are sorted lexicographically by their generated names to match +Midfall's `BTreeMap` order. + +### 13.2 Coordinate Reconstruction + +Coordinates are exposed by Midnight circuits as limbs of `coord - 1`, packed +four limbs per native field element. The x coordinate's first packed word may +carry an identity flag by adding one raw limb base. + +The verifier: + +1. Checks unused high bits in each packed word. +2. Reconstructs the shifted coordinate from limbs. +3. Detects identity encodings using the generated constants for `p - 1`. +4. For non-`p - 1` values, adds 1 to undo the `coord - 1` representation. +5. Checks the reconstructed coordinate is `< p`. +6. Checks the high EIP-2537 word fits in 128 bits. +7. Stores the point as a padded G1 tuple, or stores all zeros for identity. + +### 13.3 Accumulator Pairing Batch + +After `PAIRING_LHS` and `PAIRING_RHS` are fixed, derive: + +```text +alpha = keccak256( + "pairing-batch-acc-kzg" domain word + || PAIRING_RHS + || PAIRING_LHS + || ACC_RHS + || ACC_LHS +) mod r +``` + +If `alpha == 0`, replace it with 1. + +Then update: + +```text +PAIRING_RHS += alpha * ACC_RHS +PAIRING_LHS += alpha * ACC_LHS +``` + +The final pairing check in section 12.9 is then performed once. This prevents +two bad pairing equations from cancelling deterministically; after inputs are +fixed, a bad pair can pass for at most one `alpha`. + +## 14. Memory Architecture + +A reimplementation may choose a different internal memory map if all observable +semantics are preserved. This repository uses absolute Yul memory addresses to +keep generated bytecode compact and to simplify precompile frames. + +The codegen memory planner must ensure: + +- All regions are 32-byte aligned. +- Permanent regions never overlap. +- Scratch regions overlap only when their lifetimes are disjoint. +- PCS fixed windows cannot overflow. +- Solidity-reserved memory `[0x00..0x80)` is not used for generated writes. +- The transcript buffer starts at `0x80` and lives below `VK_MPTR`. +- The VK payload remains live until all quotient/PCS code that references it + has finished. +- External quotient evaluator frames cover every memory word the evaluator + needs. + +High-level memory order: + +```text +0x00..0x7f: + Solidity-reserved scratch, free-memory pointer, and zero slot + +0x80..VK_MPTR: + transient transcript buffer and low precompile scratch + +VK_MPTR: + copied or embedded VK payload + +after VK: + user challenge slots + +THETA_MPTR: + challenge scalars, batch-open commitments, Lagrange values, + PCS fixed windows, decoded evals, proof commitments, selector buckets, + quotient scratch, PCS scratch, accumulator scratch +``` + +Important theta-relative word offsets: + +```text +0: theta +1: beta +2: gamma +3: trash_challenge +4: y +5: x +6: x1 +7: x2 +8: x3 +9: x4 +10: f_com, 4 words +14: pi, 4 words +18: acc_lhs, 4 words +22: acc_rhs, 4 words +26: x_n +27: x_n_minus_1_inv +28: l_last +29: l_blind +30: l_0 +31: instance_eval +32: quotient_eval +33: quotient scratch, 4 words +38: f_eval +39: v +40: final_com, 4 words +44: pairing_lhs, 4 words +48: pairing_rhs, 4 words +52: rotation point window +80: x1 powers window +145: q_eval_set window +201: q_eval calldata pointer slot +209: G1 identity, 4 zero words +220: decoded evaluation buffer +220 + num_evals: decompressed proof commitments +``` + +Proof commitments are stored contiguously by category: + +```text +advice +lookup multiplicity +permutation Z +lookup helpers +lookup accumulator Z +trash commitments +quotient commitment(s) +``` + +See `docs/architecture/MEMORY_LAYOUT.md` for the detailed scratch lifetime table and update +rules. + +## 15. EIP-2537 And Precompile Requirements + +The generated verifier constructor runs smoke tests: + +- `G1ADD(identity, identity) -> identity`. +- `G1MSM([(identity, 0)]) -> identity`. +- `PAIRING_CHECK([(identity_g1, identity_g2)]) -> true`. + +Deploy only on forks/chains where EIP-2537 and `MCOPY` are available with the +exact addresses, encodings, subgroup checks, return-size behavior, and gas +schedule above. The test runner uses Prague-spec `revm` and includes direct +precompile conformance coverage for malformed G1 rejection, non-identity G1ADD, +two-term and 78-term G1MSM, true and false pairings, and pairing bilinearity. + +Every EIP-2537 call checks: + +- `staticcall` success. +- Exact return-data size. +- For pairing, returned word is 1. + +Gas caps are generated as literals instead of forwarding all remaining gas or +carrying the EIP-2537 discount table in runtime bytecode: + +- G1ADD cap: `50000`. +- G1MSM cap: `50000 + k * discount[k] * 12000 / 1000`, with the EIP-2537 + discount table for `k <= 128`. +- Pairing cap: `50000 + 60000 * num_pairs`. + +## 16. Codegen Configuration + +Cargo features: + +```text +evm +solidity-trace +solidity-gas-checkpoints +truncated-challenges +in-circuit-fewer-point-sets +outer-fewer-point-sets +outer-single-h-commitment +fewer-point-sets +rust-verifier-trace +``` + +Quotient lowering is intentionally fixed to the default IVC shape in code. The +previous experimental quotient knobs and alternate physical encodings are +not part of the public generator surface. + +Current quotient lowering defaults: + +```text +direct inline quotient identities: 4 +native heavy gate callbacks: 4 +quotient program encoding: bytes +VM CSE: off +native permutation callback: on +native lookup callback: on +structured trash suffix: on +limb VM ops: on for byte encoding +``` + +These settings are implementation choices. A reimplementation may pick a +different quotient lowering if it preserves all algebra and transcript +semantics. + +## 17. Verification Algorithm + +This is the complete verifier flow: + +1. Check ABI head offsets. +2. Check pinned VK and quotient evaluator code, if external. +3. Load or materialize VK payload. +4. Check `proof.length == proof_len`. +5. Check `instances.length == VK.num_instances`. +6. Check total calldata length exactly. +7. If accumulator is enabled, decode public accumulator points and run the + G1MSM validation/scaling work before transcript, quotient, PCS, and final + pairing work. +8. Initialize transcript. +9. Absorb VK digest, committed identity, instance count, and instances. +10. Parse proof commitments and scalars in the order in section 5, absorbing + each into the transcript and squeezing challenges in the order in section 7. +11. Range-check every scalar proof and instance word. +12. Copy all proof G1s into their generated memory slots. +13. Spill all eval scalars into `REVERSED_EVALS_MPTR`. +14. Compute Lagrange values and public `instance_eval`. +15. Reconstruct the quotient numerator and selector accumulators, either + inline or through the pinned quotient evaluator. +16. Compute `x_split = x^(n - 1)` and `1 - x^n`. +17. Build PCS queries and intermediate point sets. +18. Compute rotation points and x1 powers. +19. Compute `q_eval_set`. +20. Compute `f_eval`. +21. Compute `final_com` and `v`. +22. Stage KZG pairing inputs: + `pi` and `final_com - v*G + x3*pi`. +23. If accumulator is enabled, derive batching alpha from the KZG and already + validated accumulator G1 inputs, then add alpha-weighted accumulator pairing + inputs into the KZG pairing inputs. +24. Run final two-pair BLS12-381 pairing check. +25. Return `true`. + +Any failed condition must revert. + +## 18. Reimplementation Checklist + +A fresh implementation is compatible if it satisfies all of the following: + +- Accepts exactly the same ABI and rejects shifted or trailing calldata. +- Requires the same generated proof length. +- Uses the Solidity-facing proof layout in section 5. +- Repackages native proof bytes exactly as section 5 specifies. +- Absorbs transcript bytes in exactly the schedule in section 7. +- Samples challenges as `uint256_be(keccak256(buffer)) mod r` and reseeds the + buffer to the digest after every squeeze. +- Uses the same `truncated-challenges` rules when that feature is enabled. +- Derives the protocol plan from the constraint system as in section 8. +- Computes local public instance evaluation as in section 9. +- Evaluates gate, permutation, lookup, and trash identities in Rust order. +- Folds main and simple-selector identities with the y powers in section 10. +- Uses `-nu_y(x)` as the linearization expected eval. +- Expands quotient commitment(s) with `(1 - x^n) * x_split^i`; in the + outer single-H layout this has only the `i = 0` term. +- Constructs PCS intermediate sets, dummy queries, `q_eval_set`, `f_eval`, + `final_com`, `v`, and pairing inputs as in section 12. +- Pins every external correctness-critical artifact by runtime length and + codehash at construction/deployment time. +- Checks every scalar `< r`. +- Rejects non-canonical padded G1 coordinates before transcript absorption. +- Ensures every absorbed G1 is validated by an EIP-2537 path before success. +- Checks EIP-2537 call success and return sizes. +- Reverts on every failure. + +## 19. Test Expectations + +Minimum validation for a reimplementation: + +- Unit-test Keccak transcript equivalence against + `midnight_proofs::transcript::CircuitTranscript`. +- Unit-test native compressed proof to Solidity padded proof repacking. +- Unit-test proof layout counts against `ProofEvaluationCounts`. +- Unit-test protocol plan ordering for circuits with: + - advice queries at several rotations; + - fixed queries; + - one committed and one non-committed instance column; + - permutation columns; + - LogUp lookups; + - trash arguments; + - simple selectors. +- Unit-test PCS intermediate-set construction and dummy-query generation. +- Run Solidity compile tests with `solc 0.8.30+commit.73712a01`, `--via-ir`, + `--evm-version cancun`, and no CBOR metadata. +- Run end-to-end EVM verification on a Prague/EIP-2537 VM. +- Run negative tests for: + - wrong public input; + - mutated scalar; + - mutated G1; + - non-canonical padded coordinate; + - wrong proof length; + - trailing calldata; + - wrong VK codehash at construction; + - wrong quotient evaluator codehash at construction. + +Repository commands are documented in `README.md` and `TESTING.md`. diff --git a/proofs/solidity-verifier/docs/reference/MIDFALL_PROOFS_COMMENT_CORPUS.md b/proofs/solidity-verifier/docs/reference/MIDFALL_PROOFS_COMMENT_CORPUS.md new file mode 100644 index 000000000..dc9dfd865 --- /dev/null +++ b/proofs/solidity-verifier/docs/reference/MIDFALL_PROOFS_COMMENT_CORPUS.md @@ -0,0 +1,10966 @@ +# Midfall Proofs Comment Corpus + +Generated by `scripts/extract_midfall_comments.py` from upstream Rust comments. +This file preserves the source comments verbatim by path and line span; +verifier-relevant material is adapted inline in this repository's source +and templates. + +## Source + +- Source root: `/Users/Julien.Coolen/midfall/proofs/src` +- Git root: `/Users/Julien.Coolen/midfall` +- Git commit: `53dc872f495104046d96bdac0a690f903dc0c537` +- Git status at extraction: `clean` +- Rust source files: `57` +- Rust files with comments: `55` +- Comment blocks: `1436` +- Comment lines: `3597` + +## License And Attribution + +The comments below originate from the sibling Midfall `proofs` crate. +That crate ships Apache-2.0 and MIT license files, and some LogUp +files carry explicit Apache-2.0 SPDX/license headers. Preserve those +notices when copying or adapting comments into source files. + + +## `circuit/floor_planner/single_pass.rs` + +### `circuit/floor_planner/single_pass.rs:L18-L23` (item-doc) + +```text +A simple [`FloorPlanner`] that performs minimal optimizations. + +This floor planner is suitable for debugging circuits. It aims to reflect +the circuit "business logic" in the circuit layout as closely as possible. +It uses a single-pass layouter that does not reorder regions for optimal +packing. +``` + +### `circuit/floor_planner/single_pass.rs:L39` (item-doc) + +```text +A [`Layouter`] for a single-chip circuit. +``` + +### `circuit/floor_planner/single_pass.rs:L43` (item-doc) + +```text +Stores the starting row for each region. +``` + +### `circuit/floor_planner/single_pass.rs:L45` (item-doc) + +```text +Stores the first empty row for each column. +``` + +### `circuit/floor_planner/single_pass.rs:L47` (item-doc) + +```text +Stores the table fixed columns. +``` + +### `circuit/floor_planner/single_pass.rs:L62` (item-doc) + +```text +Creates a new single-chip layouter. +``` + +### `circuit/floor_planner/single_pass.rs:L89` (line) + +```text +Get shape of the region. +``` + +### `circuit/floor_planner/single_pass.rs:L96-L97` (line) + +```text +Lay out this region. We implement the simplest approach here: position the +region starting at the earliest row for which none of the columns are in use. +``` + +### `circuit/floor_planner/single_pass.rs:L104` (line) + +```text +Update column usage information. +``` + +### `circuit/floor_planner/single_pass.rs:L109` (line) + +```text +Assign region cells. +``` + +### `circuit/floor_planner/single_pass.rs:L119-L120` (line) + +```text +Assign constants. For the simple floor planner, we assign constants in order +in the first `constants` column. +``` + +### `circuit/floor_planner/single_pass.rs:L155-L156` (line) + +```text +Maintenance hazard: there is near-duplicate code in +`v1::AssignmentPass::assign_table`. Assign table cells. +``` + +### `circuit/floor_planner/single_pass.rs:L166-L167` (line) + +```text +Check that all table columns have the same length `first_unused`, +and all cells up to that length are assigned. +``` + +### `circuit/floor_planner/single_pass.rs:L170` (line) + +```text +Record these columns so that we can prevent them from being used again. +``` + +### `circuit/floor_planner/single_pass.rs:L176-L178` (line) + +```text +default_val must be Some because we must have assigned +at least one cell in each column, and in that case we checked +that all cells up to first_unused were assigned. +``` + +### `circuit/floor_planner/single_pass.rs:L223-L224` (item-doc) + +```text +Stores the constants to be assigned, and the cells to which they are +copied. +``` + + +## `circuit/floor_planner/v1/strategy.rs` + +### `circuit/floor_planner/v1/strategy.rs:L10` (item-doc) + +```text +A region allocated within a column. +``` + +### `circuit/floor_planner/v1/strategy.rs:L13` (line) + +```text +The starting position of the region. +``` + +### `circuit/floor_planner/v1/strategy.rs:L15` (line) + +```text +The length of the region. +``` + +### `circuit/floor_planner/v1/strategy.rs:L31` (item-doc) + +```text +An area of empty space within a column. +``` + +### `circuit/floor_planner/v1/strategy.rs:L33` (line) + +```text +The starting position (inclusive) of the empty space. +``` + +### `circuit/floor_planner/v1/strategy.rs:L35` (line) + +```text +The ending position (exclusive) of the empty space, or `None` if unbounded. +``` + +### `circuit/floor_planner/v1/strategy.rs:L45-L48` (item-doc) + +```text +Allocated rows within a column. + +This is a set of [a_start, a_end) pairs representing disjoint allocated +intervals. +``` + +### `circuit/floor_planner/v1/strategy.rs:L53-L54` (item-doc) + +```text +Returns the row that forms the unbounded unallocated interval [row, +None). +``` + +### `circuit/floor_planner/v1/strategy.rs:L59-L62` (item-doc) + +```text +Return all the *unallocated* nonempty intervals intersecting [start, +end). + +`end = None` represents an unbounded end. +``` + +### `circuit/floor_planner/v1/strategy.rs:L100` (item-doc) + +```text +Allocated rows within a circuit. +``` + +### `circuit/floor_planner/v1/strategy.rs:L103-L105` (item-doc) + +```text +- `start` is the current start row of the region (not of this column). +- `slack` is the maximum number of rows the start could be moved down, + taking into account prior columns. +``` + +### `circuit/floor_planner/v1/strategy.rs:L119-L120` (line) + +```text +Iterate over the unallocated non-empty intervals in c that intersect [start, +end). +``` + +### `circuit/floor_planner/v1/strategy.rs:L122` (line) + +```text +Do we have enough room for this column of the region in this interval? +``` + +### `circuit/floor_planner/v1/strategy.rs:L150` (line) + +```text +No placement worked; the caller will need to try other possibilities. +``` + +### `circuit/floor_planner/v1/strategy.rs:L154-L155` (item-doc) + +```text +Positions the regions starting at the earliest row for which none of the +columns are in use, taking into account gaps between earlier regions. +``` + +### `circuit/floor_planner/v1/strategy.rs:L159` (line) + +```text +Tracks the empty regions for each column. +``` + +### `circuit/floor_planner/v1/strategy.rs:L165-L167` (line) + +```text +Sort the region's columns to ensure determinism. +- An unstable sort is fine, because region.columns() returns a set. +- The sort order relies on Column's Ord implementation! +``` + +### `circuit/floor_planner/v1/strategy.rs:L184` (line) + +```text +Return the column allocations for potential further processing. +``` + +### `circuit/floor_planner/v1/strategy.rs:L188-L189` (item-doc) + +```text +Sorts the regions by advice area and then lays them out with the [`slot_in`] +strategy. +``` + +### `circuit/floor_planner/v1/strategy.rs:L195` (line) + +```text +Count the number of advice columns +``` + +### `circuit/floor_planner/v1/strategy.rs:L204` (line) + +```text +Sort by advice area (since this has the most contention). +``` + +### `circuit/floor_planner/v1/strategy.rs:L212` (line) + +```text +Lay out the sorted regions. +``` + +### `circuit/floor_planner/v1/strategy.rs:L215` (line) + +```text +Un-sort the regions so they match the original indexing. +``` + + +## `circuit/floor_planner/v1.rs` + +### `circuit/floor_planner/v1.rs:L20-L27` (item-doc) + +```text +The version 1 [`FloorPlanner`] provided by `halo2`. + +- No column optimizations are performed. Circuit configuration is left + entirely to the circuit designer. +- A dual-pass layouter is used to measures regions prior to assignment. +- Regions are measured as rectangles, bounded on the cells they assign. +- Regions are laid out using a greedy first-fit strategy, after sorting + regions by their "advice area" (number of advice columns * rows). +``` + +### `circuit/floor_planner/v1.rs:L33` (item-doc) + +```text +Stores the starting row for each region. +``` + +### `circuit/floor_planner/v1.rs:L35-L36` (item-doc) + +```text +Stores the constants to be assigned, and the cells to which they are +copied. +``` + +### `circuit/floor_planner/v1.rs:L38` (item-doc) + +```text +Stores the table fixed columns. +``` + +### `circuit/floor_planner/v1.rs:L49` (item-doc) + +```text +Creates a new v1 layouter. +``` + +### `circuit/floor_planner/v1.rs:L70` (line) + +```text +First pass: measure the regions within the circuit. +``` + +### `circuit/floor_planner/v1.rs:L79-L80` (line) + +```text +Planning: +- Position the regions. +``` + +### `circuit/floor_planner/v1.rs:L84` (line) + +```text +- Determine how many rows our planned circuit will require. +``` + +### `circuit/floor_planner/v1.rs:L91` (line) + +```text +- Position the constants within those rows. +``` + +### `circuit/floor_planner/v1.rs:L112-L113` (line) + +```text +Second pass: +- Assign the regions. +``` + +### `circuit/floor_planner/v1.rs:L120` (line) + +```text +- Assign the constants. +``` + +### `circuit/floor_planner/v1.rs:L151` (item-doc) + +```text +A single pass of the [`V1`] layouter. +``` + +### `circuit/floor_planner/v1.rs:L232` (item-doc) + +```text +Measures the circuit. +``` + +### `circuit/floor_planner/v1.rs:L249` (line) + +```text +Get shape of the region. +``` + +### `circuit/floor_planner/v1.rs:L261` (item-doc) + +```text +Assigns the circuit. +``` + +### `circuit/floor_planner/v1.rs:L265` (item-doc) + +```text +Counter tracking which region we need to assign next. +``` + +### `circuit/floor_planner/v1.rs:L283` (line) + +```text +Get the next region we are assigning. +``` + +### `circuit/floor_planner/v1.rs:L304-L305` (line) + +```text +Maintenance hazard: there is near-duplicate code in +`SingleChipLayouter::assign_table`. +``` + +### `circuit/floor_planner/v1.rs:L307` (line) + +```text +Assign table cells. +``` + +### `circuit/floor_planner/v1.rs:L317-L318` (line) + +```text +Check that all table columns have the same length `first_unused`, +and all cells up to that length are assigned. +``` + +### `circuit/floor_planner/v1.rs:L321` (line) + +```text +Record these columns so that we can prevent them from being used again. +``` + +### `circuit/floor_planner/v1.rs:L327-L329` (line) + +```text +default_val must be Some because we must have assigned +at least one cell in each column, and in that case we checked +that all cells up to first_unused were assigned. +``` + + +## `circuit/floor_planner.rs` + +### `circuit/floor_planner.rs:L1` (module-doc) + +```text +Implementations of common circuit floor planners. +``` + + +## `circuit/layouter.rs` + +### `circuit/layouter.rs:L1` (module-doc) + +```text +Implementations of common circuit layouters. +``` + +### `circuit/layouter.rs:L14` (item-doc) + +```text +Intermediate trait requirements for [`RegionLayouter`]. +``` + +### `circuit/layouter.rs:L19-L48` (item-doc) + +````text +Helper trait for implementing a custom [`Layouter`]. + +This trait is used for implementing region assignments: + +```text +impl<'a, F: Field, C: Chip, CS: Assignment + 'a> Layouter for MyLayouter<'a, C, CS> { + fn assign_region( + &mut self, + assignment: impl FnOnce(Region<'_, F, C>) -> Result<(), Error>, + ) -> Result<(), Error> { + let region_index = self.regions.len(); + self.regions.push(self.current_gate); + + let mut region = MyRegion::new(self, region_index); + { + let region: &mut dyn RegionLayouter = &mut region; + assignment(region.into())?; + } + self.current_gate += region.row_count; + + Ok(()) + } +} +``` + +TODO: It would be great if we could constrain the columns in these types to +be "logical" columns that are guaranteed to correspond to the chip (and have +come from `Chip::Config`). + +[`Layouter`]: super::Layouter +```` + +### `circuit/layouter.rs:L50` (item-doc) + +```text +Enables a selector at the given offset. +``` + +### `circuit/layouter.rs:L58-L62` (item-doc) + +```text +Allows the circuit implementor to name/annotate a Column within a Region +context. + +This is useful in order to improve the amount of information that +`prover.verify()` and `prover.assert_satisfied()` can provide. +``` + +### `circuit/layouter.rs:L69` (item-doc) + +```text +Assign an advice column value (witness) +``` + +### `circuit/layouter.rs:L78-L85` (item-doc) + +```text +Assigns a constant value to the column `advice` at `offset` within this +region. + +The constant value will be assigned to a cell within one of the fixed +columns configured via `ConstraintSystem::enable_constant`. + +Returns the advice cell that has been equality-constrained to the +constant. +``` + +### `circuit/layouter.rs:L94-L98` (item-doc) + +```text +Assign the value of the instance column's cell at absolute location +`row` to the column `advice` at `offset` within this region. + +Returns the advice cell that has been equality-constrained to the +instance cell, and its value if known. +``` + +### `circuit/layouter.rs:L108-L109` (item-doc) + +```text +Returns the value of the instance column's cell at absolute location +`row`. +``` + +### `circuit/layouter.rs:L113` (item-doc) + +```text +Assigns a fixed value +``` + +### `circuit/layouter.rs:L122-L125` (item-doc) + +```text +Constrains a cell to have a constant value. + +Returns an error if the cell is in a column where equality has not been +enabled. +``` + +### `circuit/layouter.rs:L128-L131` (item-doc) + +```text +Constraint two cells to have the same value. + +Returns an error if either of the cells is not within the given +permutation. +``` + +### `circuit/layouter.rs:L135-L136` (item-doc) + +```text +The shape of a region. For a region at a certain index, we track +the set of columns it uses as well as the number of rows it uses. +``` + +### `circuit/layouter.rs:L144-L145` (item-doc) + +```text +The virtual column involved in a region. This includes concrete columns, +as well as selectors that are not concrete columns at this stage. +``` + +### `circuit/layouter.rs:L148` (item-doc) + +```text +Concrete column +``` + +### `circuit/layouter.rs:L150` (item-doc) + +```text +Virtual column representing a (boolean) selector +``` + +### `circuit/layouter.rs:L184` (item-doc) + +```text +Create a new `RegionShape` for a region at `region_index`. +``` + +### `circuit/layouter.rs:L193` (item-doc) + +```text +Get the `region_index` of a `RegionShape`. +``` + +### `circuit/layouter.rs:L198` (item-doc) + +```text +Get a reference to the set of `columns` used in a `RegionShape`. +``` + +### `circuit/layouter.rs:L203` (item-doc) + +```text +Get the `row_count` of a `RegionShape`. +``` + +### `circuit/layouter.rs:L216` (line) + +```text +Track the selector's fixed column as part of the region's shape. +``` + +### `circuit/layouter.rs:L246` (line) + +```text +The rest is identical to witnessing an advice cell. +``` + +### `circuit/layouter.rs:L301` (line) + +```text +Do nothing +``` + +### `circuit/layouter.rs:L305` (line) + +```text +Global constants don't affect the region shape. +``` + +### `circuit/layouter.rs:L310` (line) + +```text +Equality constraints don't affect the region shape. +``` + + +## `circuit/mod.rs` + +### `circuit/mod.rs:L1` (module-doc) + +```text +Traits and structs for implementing circuit components. +``` + +### `circuit/mod.rs:L22-L29` (item-doc) + +```text +A chip implements a set of instructions that can be used by gadgets. + +The chip stores state that is required at circuit synthesis time in +[`Chip::Config`], which can be fetched via [`Chip::config`]. + +The chip also loads any fixed configuration needed at synthesis time +using its own implementation of `load`, and stores it in [`Chip::Loaded`]. +This can be accessed via [`Chip::loaded`]. +``` + +### `circuit/mod.rs:L31-L35` (item-doc) + +```text +A type that holds the configuration for this chip, and any other state +it may need during circuit synthesis, that can be derived during +[`Circuit::configure`]. + +[`Circuit::configure`]: crate::plonk::Circuit::configure +``` + +### `circuit/mod.rs:L38-L42` (item-doc) + +```text +A type that holds any general chip state that needs to be loaded at the +start of [`Circuit::synthesize`]. This might simply be `()` for some +chips. + +[`Circuit::synthesize`]: crate::plonk::Circuit::synthesize +``` + +### `circuit/mod.rs:L45` (item-doc) + +```text +The chip holds its own configuration. +``` + +### `circuit/mod.rs:L48-L51` (item-doc) + +```text +Provides access to general chip state loaded at the beginning of circuit +synthesis. + +Panics if called before `Chip::load`. +``` + +### `circuit/mod.rs:L55` (item-doc) + +```text +Index of a region in a layouter +``` + +### `circuit/mod.rs:L73` (item-doc) + +```text +Starting row of a region in a layouter +``` + +### `circuit/mod.rs:L91` (item-doc) + +```text +A pointer to a cell within a circuit. +``` + +### `circuit/mod.rs:L94` (item-doc) + +```text +Identifies the region in which this cell resides. +``` + +### `circuit/mod.rs:L96` (item-doc) + +```text +The relative offset of this cell within its region. +``` + +### `circuit/mod.rs:L98` (item-doc) + +```text +The column of this cell. +``` + +### `circuit/mod.rs:L102` (item-doc) + +```text +An assigned cell. +``` + +### `circuit/mod.rs:L111-L114` (item-doc) + +```text +Update the value of an `AssignedCell`. + +Returns an error if the cell had a value which is different from the one +we are trying to set. +``` + +### `circuit/mod.rs:L138` (item-doc) + +```text +Returns the value of the [`AssignedCell`]. +``` + +### `circuit/mod.rs:L143` (item-doc) + +```text +Returns the cell. +``` + +### `circuit/mod.rs:L153` (item-doc) + +```text +Returns the field element value of the [`AssignedCell`]. +``` + +### `circuit/mod.rs:L160-L163` (item-doc) + +```text +Evaluates this assigned cell's value directly, performing an unbatched +inversion if necessary. + +If the denominator is zero, the returned cell's value is zero. +``` + +### `circuit/mod.rs:L177-L181` (item-doc) + +```text +Converts any `AssignedCell` to an `AssignedNative`, using +back-and-forth conversion to rationals. All specific information +specific to the structure of `V` is lost. Can be useful when interacting +with external circuits that produce `AssignedCell` types that +`midnight-circuits` cannot interact with. +``` + +### `circuit/mod.rs:L196-L199` (item-doc) + +```text +Copies the value to a given advice cell and constrains them to be equal. + +Returns an error if either this cell or the given cell are in columns +where equality has not been enabled. +``` + +### `circuit/mod.rs:L219-L230` (item-doc) + +```text +A region of the circuit in which a [`Chip`] can assign cells. + +Inside a region, the chip may freely use relative offsets; the [`Layouter`] +will treat these assignments as a single "region" within the circuit. + +The [`Layouter`] is allowed to optimise between regions as it sees fit. +Chips must use [`Region::constrain_equal`] to copy in variables assigned in +other regions. + +TODO: It would be great if we could constrain the columns in these types to +be "logical" columns that are guaranteed to correspond to the chip (and have +come from `Chip::Config`). +``` + +### `circuit/mod.rs:L243` (item-doc) + +```text +Enables a selector at the given offset. +``` + +### `circuit/mod.rs:L257-L261` (item-doc) + +```text +Allows the circuit implementor to name/annotate a Column within a Region +context. + +This is useful in order to improve the amount of information that +`prover.verify()` and `prover.assert_satisfied()` can provide. +``` + +### `circuit/mod.rs:L271-L274` (item-doc) + +```text +Assign an advice column value (witness). + +Even though `to` has `FnMut` bounds, it is guaranteed to be called at +most once. +``` + +### `circuit/mod.rs:L304-L310` (item-doc) + +```text +Assigns a constant value to the column `advice` at `offset` within this +region. + +The constant value will be assigned to a cell within one of the fixed +columns configured via `ConstraintSystem::enable_constant`. + +Returns the advice cell. +``` + +### `circuit/mod.rs:L337-L340` (item-doc) + +```text +Assign the value of the instance column's cell at absolute location +`row` to the column `advice` at `offset` within this region. + +Returns the advice cell, and its value if known. +``` + +### `circuit/mod.rs:L368-L374` (item-doc) + +```text +Returns the value of the instance column's cell at absolute location +`row`. + +This method is only provided for convenience; it does not create any +constraints. Callers still need to use +[`Self::assign_advice_from_instance`] to constrain the +instance values in their circuit. +``` + +### `circuit/mod.rs:L383-L386` (item-doc) + +```text +Assign a fixed value. + +Even though `to` has `FnMut` bounds, it is guaranteed to be called at +most once. +``` + +### `circuit/mod.rs:L416-L419` (item-doc) + +```text +Constrains a cell to have a constant value. + +Returns an error if the cell is in a column where equality has not been +enabled. +``` + +### `circuit/mod.rs:L427-L430` (item-doc) + +```text +Constrains two cells to have the same value. + +Returns an error if either of the cells are in columns where equality +has not been enabled. +``` + +### `circuit/mod.rs:L436` (item-doc) + +```text +A lookup table in the circuit. +``` + +### `circuit/mod.rs:L449-L454` (item-doc) + +```text +Assigns a fixed value to a table cell. + +Returns an error if the table cell has already been assigned to. + +Even though `to` has `FnMut` bounds, it is guaranteed to be called at +most once. +``` + +### `circuit/mod.rs:L474-L477` (item-doc) + +```text +A layout strategy within a circuit. The layouter is chip-agnostic and +applies its strategy to the context and config it is given. + +This abstracts over the circuit assignments, handling row indices etc. +``` + +### `circuit/mod.rs:L479-L480` (item-doc) + +```text +Represents the type of the "root" of this layouter, so that nested +namespaces can minimize indirection. +``` + +### `circuit/mod.rs:L483-L495` (item-doc) + +````text +Assign a region of gates to an absolute row number. + +Inside the closure, the chip may freely use relative offsets; the +`Layouter` will treat these assignments as a single "region" within +the circuit. Outside this closure, the `Layouter` is allowed to +optimise as it sees fit. + +```text +fn assign_region(&mut self, || "region name", |region| { + let config = chip.config(); + region.assign_advice(config.a, offset, || { Some(value)}); +}); +``` +```` + +### `circuit/mod.rs:L502-L509` (item-doc) + +````text +Assign a table region to an absolute row number. + +```text +fn assign_table(&mut self, || "table name", |table| { + let config = chip.config(); + table.assign_fixed(config.a, offset, || { Some(value)}); +}); +``` +```` + +### `circuit/mod.rs:L516-L517` (item-doc) + +```text +Constrains a [`Cell`] to equal an instance column's row value at an +absolute position. +``` + +### `circuit/mod.rs:L525-L528` (item-doc) + +```text +Queries the value of the given challenge. + +Returns `Value::unknown()` if the current synthesis phase is before the +challenge can be queried. +``` + +### `circuit/mod.rs:L531-L534` (item-doc) + +```text +Gets the "root" of this assignment, bypassing the namespacing. + +Not intended for downstream consumption; use [`Layouter::namespace`] +instead. +``` + +### `circuit/mod.rs:L537-L540` (item-doc) + +```text +Creates a new (sub)namespace and enters into it. + +Not intended for downstream consumption; use [`Layouter::namespace`] +instead. +``` + +### `circuit/mod.rs:L546-L549` (item-doc) + +```text +Exits out of the existing namespace. + +Not intended for downstream consumption; use [`Layouter::namespace`] +instead. +``` + +### `circuit/mod.rs:L552` (item-doc) + +```text +Enters into a namespace. +``` + +### `circuit/mod.rs:L564-L565` (item-doc) + +```text +This is a "namespaced" layouter which borrows a `Layouter` (pushing a +namespace context) and, when dropped, pops out of the namespace context. +``` + + +## `circuit/table_layouter.rs` + +### `circuit/table_layouter.rs:L1` (module-doc) + +```text +Implementations of common table layouters. +``` + +### `circuit/table_layouter.rs:L16-L20` (item-doc) + +```text +Helper trait for implementing a custom [`Layouter`]. + +This trait is used for implementing table assignments. + +[`Layouter`]: super::Layouter +``` + +### `circuit/table_layouter.rs:L22-L24` (item-doc) + +```text +Assigns a fixed value to a table cell. + +Returns an error if the table cell has already been assigned to. +``` + +### `circuit/table_layouter.rs:L34-L40` (item-doc) + +```text +The default value to fill a table column with. + +- The outer `Option` tracks whether the value in row 0 of the table column + has been assigned yet. This will always be `Some` once a valid table has + been completely assigned. +- The inner `Value` tracks whether the underlying `Assignment` is evaluating + witnesses or not. +``` + +### `circuit/table_layouter.rs:L43` (item-doc) + +```text +A table layouter that can be used to assign values to a table. +``` + +### `circuit/table_layouter.rs:L47-L48` (item-doc) + +```text +maps from a fixed column to a pair (default value, vector saying which +rows are assigned) +``` + +### `circuit/table_layouter.rs:L62` (item-doc) + +```text +Returns a new SimpleTableLayouter +``` + +### `circuit/table_layouter.rs:L99` (line) + +```text +Use the value at offset 0 as the default value for this table column. +``` + +### `circuit/table_layouter.rs:L101-L102` (line) + +```text +Since there is already an existing default value for this table column, +the caller should not be attempting to assign another value at offset 0. +``` + +### `circuit/table_layouter.rs:L131` (line) + +```text +All values in the column have been assigned +``` + + +## `circuit/value.rs` + +### `circuit/value.rs:L10-L17` (item-doc) + +```text +A value that might exist within a circuit. + +This behaves like `Option` but differs in two key ways: +- It does not expose the enum cases, or provide an `Option::unwrap` + equivalent. This helps to ensure that unwitnessed values correctly + propagate. +- It provides pass-through implementations of common traits such as `Add` + and `Mul`, for improved usability. +``` + +### `circuit/value.rs:L30` (item-doc) + +```text +Constructs an unwitnessed value. +``` + +### `circuit/value.rs:L35-L43` (item-doc) + +````text +Constructs a known value. + +# Examples + +``` +use midnight_proofs::circuit::Value; + +let v = Value::known(37); +``` +```` + +### `circuit/value.rs:L48-L50` (item-doc) + +```text +Obtains the inner value for assigning into the circuit. + +Returns `Error::Synthesis` if this is [`Value::unknown()`]. +``` + +### `circuit/value.rs:L55` (item-doc) + +```text +Converts from `&Value` to `Value<&V>`. +``` + +### `circuit/value.rs:L62` (item-doc) + +```text +Converts from `&mut Value` to `Value<&mut V>`. +``` + +### `circuit/value.rs:L69` (item-doc) + +```text +ONLY FOR INTERNAL CRATE USAGE; DO NOT EXPOSE! +``` + +### `circuit/value.rs:L74-L81` (item-doc) + +```text +Enforces an assertion on the contained value, if known. + +The assertion is ignored if `self` is [`Value::unknown()`]. Do not try +to enforce circuit constraints with this method! + +# Panics + +Panics if `f` returns `false`. +``` + +### `circuit/value.rs:L88-L91` (item-doc) + +```text +Checks the contained value for an error condition, if known. + +The error check is ignored if `self` is [`Value::unknown()`]. Do not try +to enforce circuit constraints with this method! +``` + +### `circuit/value.rs:L99-L100` (item-doc) + +```text +Maps a `Value` to `Value` by applying a function to the contained +value. +``` + +### `circuit/value.rs:L107-L108` (item-doc) + +```text +Maps a `Value` to `Value` by applying a function to the contained +value (that returns Result). +``` + +### `circuit/value.rs:L119-L120` (item-doc) + +```text +Returns [`Value::unknown()`] if the value is [`Value::unknown()`], +otherwise calls `f` with the wrapped value and returns the result. +``` + +### `circuit/value.rs:L128-L132` (item-doc) + +```text +Zips `self` with another `Value`. + +If `self` is `Value::known(s)` and `other` is `Value::known(o)`, this +method returns `Value::known((s, o))`. Otherwise, +[`Value::unknown()`] is returned. +``` + +### `circuit/value.rs:L141-L145` (item-doc) + +```text +Unzips a value containing a tuple of two values. + +If `self` is `Value::known((a, b)), this method returns +`(Value::known(a), Value::known(b))`. Otherwise, +`(Value::unknown(), Value::unknown())` is returned. +``` + +### `circuit/value.rs:L155` (item-doc) + +```text +Maps a `Value<&V>` to a `Value` by copying the contents of the value. +``` + +### `circuit/value.rs:L166` (item-doc) + +```text +Maps a `Value<&V>` to a `Value` by cloning the contents of the value. +``` + +### `circuit/value.rs:L179-L180` (item-doc) + +```text +Maps a `Value<&mut V>` to a `Value` by copying the contents of the +value. +``` + +### `circuit/value.rs:L191-L192` (item-doc) + +```text +Maps a `Value<&mut V>` to a `Value` by cloning the contents of the +value. +``` + +### `circuit/value.rs:L205-L207` (item-doc) + +```text +Transposes a `Value<[V; LEN]>` into a `[Value; LEN]`. + +[`Value::unknown()`] will be mapped to `[Value::unknown(); LEN]`. +``` + +### `circuit/value.rs:L224-L231` (item-doc) + +```text +Transposes a `Value>` into a +`Vec>`. + +[`Value::unknown()`] will be mapped to `vec![Value::unknown(); length]`. + +# Panics + +Panics if `self` is `Value::known(values)` and `|values| != length`. +``` + +### `circuit/value.rs:L244-L246` (line) + +```text + +FromIterator + +``` + +### `circuit/value.rs:L249-L252` (item-doc) + +```text +Takes each element in the [`Iterator`]: if it is [`Value::unknown()`], +no further elements are taken, and the [`Value::unknown()`] is +returned. Should no [`Value::unknown()`] occur, a container of type +`V` containing the values of each [`Value`] is returned. +``` + +### `circuit/value.rs:L260-L262` (line) + +```text + +Neg + +``` + +### `circuit/value.rs:L274-L276` (line) + +```text + +Add + +``` + +### `circuit/value.rs:L352-L354` (line) + +```text + +Sub + +``` + +### `circuit/value.rs:L430-L432` (line) + +```text + +Mul + +``` + +### `circuit/value.rs:L508-L510` (line) + +```text + +Rational + +``` + +### `circuit/value.rs:L629` (item-doc) + +```text +Returns the field element corresponding to this value. +``` + +### `circuit/value.rs:L639` (item-doc) + +```text +Returns the field element corresponding to this value. +``` + +### `circuit/value.rs:L649-L662` (item-doc) + +````text +Doubles this field element. + +# Examples + +If you have a `Value`, convert it to `Value>` +first: +``` +# use midnight_curves::Fq as F; +use midnight_proofs::{circuit::Value, utils::rational::Rational}; + +let v = Value::known(F::from(2)); +let v: Value> = v.into(); +v.double(); +``` +```` + +### `circuit/value.rs:L672` (item-doc) + +```text +Squares this field element. +``` + +### `circuit/value.rs:L682` (item-doc) + +```text +Cubes this field element. +``` + +### `circuit/value.rs:L692` (item-doc) + +```text +Inverts this assigned value (taking the inverse of zero to be zero). +``` + +### `circuit/value.rs:L704-L707` (item-doc) + +```text +Evaluates this value directly, performing an unbatched inversion if +necessary. + +If the denominator is zero, the returned value is zero. +``` + + +## `dev/cost_model.rs` + +### `dev/cost_model.rs:L1-L2` (module-doc) + +```text +The cost estimator takes high-level parameters for a circuit design, and +estimates the verification cost, as well as resulting proof size. +``` + +### `dev/cost_model.rs:L30` (item-doc) + +```text +Options to build a circuit specification to measure the cost model of. +``` + +### `dev/cost_model.rs:L33` (item-doc) + +```text +An advice column with the given rotations. May be repeated. +``` + +### `dev/cost_model.rs:L36` (item-doc) + +```text +An instance column with the given rotations. May be repeated. +``` + +### `dev/cost_model.rs:L39` (item-doc) + +```text +How many of the instance columns are given in committed form. +``` + +### `dev/cost_model.rs:L42` (item-doc) + +```text +A fixed column with the given rotations. May be repeated. +``` + +### `dev/cost_model.rs:L45` (item-doc) + +```text +Maximum degree of the constraint system. +``` + +### `dev/cost_model.rs:L48-L49` (item-doc) + +```text +A lookup over N columns with max input degree I and max table degree T. +May be repeated. +``` + +### `dev/cost_model.rs:L52` (item-doc) + +```text +A permutation over N columns. May be repeated. +``` + +### `dev/cost_model.rs:L55` (item-doc) + +```text +Trash arguments (one per additive-selector gate). +``` + +### `dev/cost_model.rs:L58-L59` (item-doc) + +```text +2^K bound on the number of rows, accounting for ZK, PIs and Lookup +tables. +``` + +### `dev/cost_model.rs:L62-L63` (item-doc) + +```text +Rows count, not including table rows and not accounting for compression +(where multiple regions can use the same rows). +``` + +### `dev/cost_model.rs:L66-L68` (item-doc) + +```text +Table rows count, not accounting for compression (where multiple regions +can use the same rows), but not much if any compression can happen with +table rows anyway. +``` + +### `dev/cost_model.rs:L71-L72` (item-doc) + +```text +Number of rows that are devoted to blinding factors and cannot be used +for "computing". +``` + +### `dev/cost_model.rs:L76` (item-doc) + +```text +Structure holding polynomial related data for benchmarks +``` + +### `dev/cost_model.rs:L79` (item-doc) + +```text +Rotations for the given polynomial +``` + +### `dev/cost_model.rs:L94-L97` (item-doc) + +```text +Structure holding the Lookup related data for circuit benchmarks. + +Each lookup argument has shared multiplicity `m(X)` and accumulator `Z(X)` +polynomials, plus one helper polynomial `h_i(X)` per degree-bounded chunk. +``` + +### `dev/cost_model.rs:L104-L109` (item-doc) + +```text +Returns the queries of the LogUp lookup argument. + +Per argument: + - 1 multiplicities polynomial at x, + - `num_chunks` helper polynomials at x, + - 1 accumulator polynomial at x and ωx. +``` + +### `dev/cost_model.rs:L120-L121` (item-doc) + +```text +Number of commitments: +1 (multiplicities) + num_chunks (helpers) + 1 (Z). +``` + +### `dev/cost_model.rs:L126-L127` (item-doc) + +```text +Number of evaluations: +1 (multiplicities) + num_chunks (helpers) + 1 (Z at x) + 1 (Z at ωx). +``` + +### `dev/cost_model.rs:L133-L135` (item-doc) + +```text +Structure holding the Trash argument data for circuit benchmarks. + +Each trash argument contributes 1 commitment and 1 evaluation. +``` + +### `dev/cost_model.rs:L139` (item-doc) + +```text +Number of permutation enabled columns +``` + +### `dev/cost_model.rs:L144` (item-doc) + +```text +Number of usable rows. See [here](https://zcash.github.io/halo2/design/proving-system/permutation.html#zero-knowledge-adjustment) +``` + +### `dev/cost_model.rs:L149` (item-doc) + +```text +Returns the queries of the Permutation argument +``` + +### `dev/cost_model.rs:L151-L152` (line) + +```text +- at wX, X, uwX for all (except the last) +- at wX, X for the last +``` + +### `dev/cost_model.rs:L164` (item-doc) + +```text +High-level specifications of an abstract circuit. +``` + +### `dev/cost_model.rs:L167` (item-doc) + +```text +Power-of-2 bound on the number of rows in the circuit. +``` + +### `dev/cost_model.rs:L169` (item-doc) + +```text +Number of rows in the circuit (not including table rows). +``` + +### `dev/cost_model.rs:L171` (item-doc) + +```text +Number of table rows in the circuit. +``` + +### `dev/cost_model.rs:L173-L174` (item-doc) + +```text +Number of rows that are devoted to blinding factors and cannot be used +for "computing". +``` + +### `dev/cost_model.rs:L176` (item-doc) + +```text +Maximum degree of the circuit. +``` + +### `dev/cost_model.rs:L178` (item-doc) + +```text +Number of advice columns. +``` + +### `dev/cost_model.rs:L180-L181` (item-doc) + +```text +Number of fixed columns. This includes selectors, tables (for lookups), +and permutation commitments. +``` + +### `dev/cost_model.rs:L183` (item-doc) + +```text +Number of lookup arguments. +``` + +### `dev/cost_model.rs:L185` (item-doc) + +```text +Number of trash arguments. +``` + +### `dev/cost_model.rs:L187-L188` (item-doc) + +```text +Equality constraint enabled columns (fixed columns are counted in +`fixed_columns` value). +``` + +### `dev/cost_model.rs:L190` (item-doc) + +```text +Number of distinct column queries across all gates. +``` + +### `dev/cost_model.rs:L192` (item-doc) + +```text +Number of distinct sets of points in the multiopening argument. +``` + +### `dev/cost_model.rs:L194` (item-doc) + +```text +Size of the proof for the circuit +``` + +### `dev/cost_model.rs:L199-L200` (item-doc) + +```text +Convert [CostOptions] to [CircuitModel]. The proof sizè is computed +depending on the base and scalar field size of the curve used. +``` + +### `dev/cost_model.rs:L222-L230` (line) + +```text +PLONK: +- COMM bytes per advice column +- SCALAR bytes per advice column per query +- SCALAR bytes per committed instance column per query +- SCALAR bytes per fixed column per query +- SCALAR bytes per permutation column +- Per permutation chunk: 1 COMM + 3 SCALAR (last chunk has 2 SCALAR) +- Per lookup argument: (num_chunks + 2) COMM + (num_chunks + 3) SCALAR +- Per trash argument: 1 COMM + 1 SCALAR +``` + +### `dev/cost_model.rs:L259-L260` (line) + +```text +Commitments to quotient limbs: +- (max_deg - 1) COMM bytes for the limbs +``` + +### `dev/cost_model.rs:L263-L266` (line) + +```text +Multiopening argument: +- COMM bytes for f_commitment +- SCALAR bytes per set of points in multiopen argument +- COMM bytes for proof +``` + +### `dev/cost_model.rs:L289` (line) + +```text +Note that we have one fixed commitment per column in the permutation argument +``` + +### `dev/cost_model.rs:L301` (item-doc) + +```text +Given a Plonk circuit, this function returns a [CircuitModel] +``` + +### `dev/cost_model.rs:L314-L316` (item-doc) + +```text +Given a circuit, this function returns [CostOptions]. If no upper bound for +`k` is provided, we iterate until a valid `k` is found (this might delay the +computation). +``` + +### `dev/cost_model.rs:L326` (line) + +```text +init the fixed polynomials with no rotations +``` + +### `dev/cost_model.rs:L337-L338` (line) + +```text +init the advice polynomials with at least X as a rotation (always opens at +least once) +``` + +### `dev/cost_model.rs:L348` (line) + +```text +init the instance polynomials with no rotations +``` + +### `dev/cost_model.rs:L374-L375` (line) + +```text +Note that this computation does't assume that `regions` is already in +order of increasing row indices. +``` + +### `dev/cost_model.rs:L380` (line) + +```text +If `region.rows == None`, then that region has no rows. +``` + +### `dev/cost_model.rs:L382-L384` (line) + +```text +Note that `end` is the index of the last column, so when +counting rows this last column needs to be counted via `end + +1`. +``` + +### `dev/cost_model.rs:L386-L389` (line) + +```text +A region is a _table region_ if all of its columns are `Fixed` +columns (see that [`plonk::circuit::TableColumn` is a wrapper +around `Column`]). All of a table region's rows are +counted towards `table_rows_count.` +``` + +### `dev/cost_model.rs:L433-L436` (line) + +```text +DevAssembly is only used to compute the cost model, meaning that we only care +about the number of assignments and not the assignments themselves. +Therefore, we only keep track of the number of rows, the regions and the +phases, and ignore we values of the trace. +``` + +### `dev/cost_model.rs:L440` (item-doc) + +```text +The regions in the circuit. +``` + +### `dev/cost_model.rs:L447-L448` (item-doc) + +```text +Runs a synthetic keygen-and-prove operation on the given circuit, +collecting data about the constraints and their assignments. +``` + +### `dev/cost_model.rs:L516` (line) + +```text +Do nothing +``` + +### `dev/cost_model.rs:L532-L533` (line) + +```text +Track that this selector was enabled. We require that all selectors are +enabled inside some region (i.e. no floating selectors). +``` + +### `dev/cost_model.rs:L641` (line) + +```text +Do nothing; we don't care about namespaces in this context. +``` + +### `dev/cost_model.rs:L645` (line) + +```text +Do nothing; we don't care about namespaces in this context. +``` + +### `dev/cost_model.rs:L649-L651` (item-doc) + +```text +This function makes a dummy pass on the synthesize function associated to +the given circuit. This could be useful for checking that the circuit is +well-formed. +``` + +### `dev/cost_model.rs:L810-L811` (line) + +```text +Assign instances. We are just forcing the number of instances to be +determined by the variable `NB_PUBLIC_INPUTS`. +``` + +### `dev/cost_model.rs:L904` (line) + +```text +nb of unusable rows for this circuit is 8. +``` + + +## `dev/failure/emitter.rs` + +### `dev/failure/emitter.rs:L34-L38` (item-doc) + +```text +Renders a cell layout around a given failure location. + +`highlight_row` is called at the end of each row, with the offset of the +active row (if `location` is in a region), and the rotation of the current +row relative to the active row. +``` + +### `dev/failure/emitter.rs:L49-L50` (line) + +```text +If we are in a region, show rows at offsets relative to it. Otherwise, just +show the rotations directly. +``` + +### `dev/failure/emitter.rs:L89-L90` (line) + +```text +Print the assigned cells, and their region offset or rotation + the column +name at which they're assigned to. +``` + +### `dev/failure/emitter.rs:L155` (line) + +```text +This is most likely a merged selector +``` + +### `dev/failure/emitter.rs:L158` (line) + +```text +No idea how we'd get here... +``` + + +## `dev/failure.rs` + +### `dev/failure.rs:L21-L22` (item-doc) + +```text +The location within the circuit at which a particular [`VerifyFailure`] +occurred. +``` + +### `dev/failure.rs:L25` (item-doc) + +```text +A location inside a region. +``` + +### `dev/failure.rs:L27` (item-doc) + +```text +The region in which the failure occurred. +``` + +### `dev/failure.rs:L29-L30` (item-doc) + +```text +The offset (relative to the start of the region) at which the +failure occurred. +``` + +### `dev/failure.rs:L33` (item-doc) + +```text +A location outside of a region. +``` + +### `dev/failure.rs:L35` (item-doc) + +```text +The circuit row on which the failure occurred. +``` + +### `dev/failure.rs:L52` (item-doc) + +```text +Returns a `DebugColumn` from Column metadata and `&self`. +``` + +### `dev/failure.rs:L94-L95` (item-doc) + +```text +Figures out whether the given row and columns overlap an assigned +region. +``` + +### `dev/failure.rs:L106-L110` (line) + +```text +We match the region if any input columns overlap, rather than all of +them, because matching complex selector columns is hard. As long as +regions are rectangles, and failures occur due to assignments entirely +within single regions, "any" will be equivalent to "all". If these +assumptions change, we'll start getting bug reports from users :) +``` + +### `dev/failure.rs:L113` (line) + +```text +Zero-area region +``` + +### `dev/failure.rs:L125` (item-doc) + +```text +The reasons why a particular circuit is not satisfied. +``` + +### `dev/failure.rs:L128` (item-doc) + +```text +A cell used in an active gate was not assigned to. +``` + +### `dev/failure.rs:L130` (item-doc) + +```text +The index of the active gate. +``` + +### `dev/failure.rs:L132` (item-doc) + +```text +The region in which this cell should be assigned. +``` + +### `dev/failure.rs:L134-L135` (item-doc) + +```text +The offset (relative to the start of the region) at which the active +gate queries this cell. +``` + +### `dev/failure.rs:L137` (item-doc) + +```text +The column in which this cell should be assigned. +``` + +### `dev/failure.rs:L139-L142` (item-doc) + +```text +The offset (relative to the start of the region) at which this cell +should be assigned. This may be negative (for example, if a +selector enables a gate at offset 0, but the gate uses +`Rotation::prev()`). +``` + +### `dev/failure.rs:L145` (item-doc) + +```text +An instance cell used in an active gate was not assigned to. +``` + +### `dev/failure.rs:L147` (item-doc) + +```text +The index of the active gate. +``` + +### `dev/failure.rs:L149` (item-doc) + +```text +The region in which this gate was activated. +``` + +### `dev/failure.rs:L151-L152` (item-doc) + +```text +The offset (relative to the start of the region) at which the active +gate queries this cell. +``` + +### `dev/failure.rs:L154` (item-doc) + +```text +The column in which this cell should be assigned. +``` + +### `dev/failure.rs:L156` (item-doc) + +```text +The absolute row at which this cell should be assigned. +``` + +### `dev/failure.rs:L159` (item-doc) + +```text +A constraint was not satisfied for a particular row. +``` + +### `dev/failure.rs:L161` (item-doc) + +```text +The polynomial constraint that is not satisfied. +``` + +### `dev/failure.rs:L163-L167` (item-doc) + +```text +The location at which this constraint is not satisfied. + +`FailureLocation::OutsideRegion` is usually caused by a constraint +that does not contain a selector, and as a result is active +on every row. +``` + +### `dev/failure.rs:L169` (item-doc) + +```text +The values of the virtual cells used by this constraint. +``` + +### `dev/failure.rs:L172-L173` (item-doc) + +```text +A constraint was active on an unusable row, and is likely missing a +selector. +``` + +### `dev/failure.rs:L175` (item-doc) + +```text +The polynomial constraint that is not satisfied. +``` + +### `dev/failure.rs:L178` (item-doc) + +```text +A lookup input did not exist in its corresponding table. +``` + +### `dev/failure.rs:L180` (item-doc) + +```text +The name of the lookup that is not satisfied. +``` + +### `dev/failure.rs:L182-L184` (item-doc) + +```text +The index of the lookup that is not satisfied. These indices are +assigned in the order in which `ConstraintSystem::lookup` is +called during `Circuit::configure`. +``` + +### `dev/failure.rs:L186` (item-doc) + +```text +The index of the parallel lookup column that is not satisfied. +``` + +### `dev/failure.rs:L188-L201` (item-doc) + +```text +The location at which the lookup is not satisfied. + +`FailureLocation::InRegion` is most common, and may be due to the +intentional use of a lookup (if its inputs are conditional +on a complex selector), or an unintentional lookup +constraint that overlaps the region (indicating that the +lookup's inputs should be made conditional). + +`FailureLocation::OutsideRegion` is uncommon, and could mean that: +- The input expressions do not correctly constrain a default value + that exists in the table when the lookup is not being used. +- The input expressions use a column queried at a non-zero + `Rotation`, and the lookup is active on a row adjacent to an + unrelated region. +``` + +### `dev/failure.rs:L204` (item-doc) + +```text +A permutation did not preserve the original value of a cell. +``` + +### `dev/failure.rs:L206` (item-doc) + +```text +The column in which this permutation is not satisfied. +``` + +### `dev/failure.rs:L208` (item-doc) + +```text +The location at which the permutation is not satisfied. +``` + +### `dev/failure.rs:L334-L345` (item-doc) + +````text +Renders `VerifyFailure::CellNotAssigned`. + +```text +error: cell not assigned + Cell layout in region 'Faulty synthesis': + | Offset | A0 | A1 | + +--------+----+----+ + | 0 | x0 | | + | 1 | | X | <--{ X marks the spot! 🦜 + + Gate 'Equality check' (applied at offset 1) queries these cells. +``` +```` + +### `dev/failure.rs:L354-L357` (line) + +```text +Collect the necessary rendering information: +- The columns involved in this gate. +- How many cells are in each column. +- The grid of cell values, indexed by rotation. +``` + +### `dev/failure.rs:L398-L414` (item-doc) + +````text +Renders `VerifyFailure::ConstraintNotSatisfied`. + +```text +error: constraint not satisfied + Cell layout in region 'somewhere': + | Offset | A0 | + +--------+----+ + | 0 | x0 | <--{ Gate 'foo' applied here + | 1 | x1 | + + Constraint 'bar': + x1 + x1 * 0x100 + x1 * 0x10000 + x1 * 0x100_0000 - x0 = 0 + + Assigned cell values: + x0 = 0x5 + x1 = 0x5 +``` +```` + +### `dev/failure.rs:L421-L424` (line) + +```text +Collect the necessary rendering information: +- The columns involved in this constraint. +- How many cells are in each column. +- The grid of cell values, indexed by rotation. +``` + +### `dev/failure.rs:L443` (line) + +```text +Print the unsatisfied constraint, in terms of the local variables. +``` + +### `dev/failure.rs:L454` (line) + +```text +Print the map from local variables to assigned values. +``` + +### `dev/failure.rs:L462-L479` (item-doc) + +````text +Renders `VerifyFailure::Lookup`. + +```text +error: lookup input does not exist in table + (L0) ∉ (F0) + + Lookup inputs: + L0 = x1 * x0 + (1 - x1) * 0x2 + ^ + | Cell layout in region 'Faulty synthesis': + | | Offset | A0 | F1 | + | +--------+----+----+ + | | 1 | x0 | x1 | <--{ Lookup inputs queried here + | + | Assigned cell values: + | x0 = 0x5 + | x1 = 1 +``` +```` + +### `dev/failure.rs:L491-L492` (line) + +```text +Get the absolute row on which the lookup's inputs are being queried, so we +can fetch the input values. +``` + +### `dev/failure.rs:L500-L501` (line) + +```text +Recover the fixed columns from the table expressions. We don't allow +composite expressions for the table side of lookups. +``` + +### `dev/failure.rs:L584` (line) + +```text +Fetch the cell values (since we don't store them in VerifyFailure::Lookup). +``` + +### `dev/failure.rs:L609-L612` (line) + +```text +Collect the necessary rendering information: +- The columns involved in this constraint. +- How many cells are in each column. +- The grid of cell values, indexed by rotation. +``` + +### `dev/failure.rs:L640` (line) + +```text +Print the map from local variables to assigned values. +``` + +### `dev/failure.rs:L650` (item-doc) + +```text +Emits this failure in pretty-printed format to stderr. +``` + + +## `dev/gates.rs` + +### `dev/gates.rs:L26-L96` (item-doc) + +````text +A struct for collecting and displaying the gates within a circuit. + +# Examples + +``` +use ff::Field; +use midnight_curves::Fq; +use midnight_proofs::{ + circuit::{Layouter, SimpleFloorPlanner}, + dev::CircuitGates, + plonk::{Circuit, ConstraintSystem, Constraints, Error}, + poly::Rotation, +}; + +#[derive(Copy, Clone)] +struct MyConfig {} + +#[derive(Clone, Default)] +struct MyCircuit {} + +impl Circuit for MyCircuit { + type Config = MyConfig; + type FloorPlanner = SimpleFloorPlanner; + #[cfg(feature = "circuit-params")] + type Params = (); + + fn without_witnesses(&self) -> Self { + Self::default() + } + + fn configure(meta: &mut ConstraintSystem) -> MyConfig { + let a = meta.advice_column(); + let b = meta.advice_column(); + let c = meta.advice_column(); + let s = meta.selector(); + + meta.create_gate("R1CS constraint", |meta| { + let a = meta.query_advice(a, Rotation::cur()); + let b = meta.query_advice(b, Rotation::cur()); + let c = meta.query_advice(c, Rotation::cur()); + + Constraints::with_selector(s, vec![("R1CS", (a * b - c))]) + }); + + // We aren't using this circuit for anything in this example. + MyConfig {} + } + + fn synthesize(&self, _: MyConfig, _: impl Layouter) -> Result<(), Error> { + // Gates are known at configure time; it doesn't matter how we use them. + Ok(()) + } +} + +#[cfg(feature = "circuit-params")] +let gates = CircuitGates::collect::(()); +#[cfg(not(feature = "circuit-params"))] +let gates = CircuitGates::collect::(); +assert_eq!( + format!("{}", gates), + r#####"R1CS constraint: +- R1CS: + S0 * (A0@0 * A1@0 - A2@0) +Total gates: 1 +Total custom constraint polynomials: 1 +Total negations: 1 +Total additions: 1 +Total multiplications: 2 +"#####, +); +``` +```` + +### `dev/gates.rs:L106` (item-doc) + +```text +Collects the gates from within the circuit. +``` + +### `dev/gates.rs:L110` (line) + +```text +Collect the graph details. +``` + +### `dev/gates.rs:L252` (item-doc) + +```text +Prints the queries in this circuit to a CSV grid. +``` + + +## `dev/graph.rs` + +### `dev/graph.rs:L15-L20` (item-doc) + +```text +Builds a dot graph string representing the given circuit. + +The graph is built from calls to [`Layouter::namespace`] both within the +circuit, and inside the gadgets and chips that it uses. + +[`Layouter::namespace`]: crate::circuit::Layouter#method.namespace +``` + +### `dev/graph.rs:L24` (line) + +```text +Collect the graph details. +``` + +### `dev/graph.rs:L33-L34` (line) + +```text +Construct the node labels. We need to store these, because tabbycat operates +on string references, and we need those references to live long enough. +``` + +### `dev/graph.rs:L47` (line) + +```text +Construct the dot graph statements. +``` + +### `dev/graph.rs:L61` (line) + +```text +Build the graph! +``` + +### `dev/graph.rs:L74` (item-doc) + +```text +Graph nodes in the namespace, structured as `(name, gadget_name)`. +``` + +### `dev/graph.rs:L77` (item-doc) + +```text +Directed edges in the graph, as pairs of indices into `nodes`. +``` + +### `dev/graph.rs:L80` (item-doc) + +```text +The current namespace, as indices into `nodes`. +``` + +### `dev/graph.rs:L90` (line) + +```text +Do nothing; we don't care about regions in this context. +``` + +### `dev/graph.rs:L94` (line) + +```text +Do nothing; we don't care about regions in this context. +``` + +### `dev/graph.rs:L102` (line) + +```text +Do nothing; we don't care about cells in this context. +``` + +### `dev/graph.rs:L111` (line) + +```text +Do nothing +``` + +### `dev/graph.rs:L131` (line) + +```text +Do nothing; we don't care about cells in this context. +``` + +### `dev/graph.rs:L148` (line) + +```text +Do nothing; we don't care about cells in this context. +``` + +### `dev/graph.rs:L159` (line) + +```text +Do nothing; we don't care about permutations in this context. +``` + +### `dev/graph.rs:L181` (line) + +```text +Store the new node. +``` + +### `dev/graph.rs:L185` (line) + +```text +Create an edge from the parent, if any. +``` + +### `dev/graph.rs:L190` (line) + +```text +Push the new namespace. +``` + +### `dev/graph.rs:L195` (line) + +```text +Store the gadget name that was extracted, if any. +``` + +### `dev/graph.rs:L202` (line) + +```text +Pop the namespace. +``` + + +## `dev/metadata.rs` + +### `dev/metadata.rs:L1` (module-doc) + +```text +Metadata about circuits. +``` + +### `dev/metadata.rs:L10` (item-doc) + +```text +Metadata about a column within a circuit. +``` + +### `dev/metadata.rs:L13` (item-doc) + +```text +The type of the column. +``` + +### `dev/metadata.rs:L15` (item-doc) + +```text +The index of the column. +``` + +### `dev/metadata.rs:L20` (item-doc) + +```text +Return the column type. +``` + +### `dev/metadata.rs:L24` (item-doc) + +```text +Return the column index. +``` + +### `dev/metadata.rs:L51-L52` (item-doc) + +```text +A helper structure that allows to print a Column with it's annotation as a +single structure. +``` + +### `dev/metadata.rs:L55` (item-doc) + +```text +The type of the column. +``` + +### `dev/metadata.rs:L57` (item-doc) + +```text +The index of the column. +``` + +### `dev/metadata.rs:L59` (item-doc) + +```text +Annotation of the column +``` + +### `dev/metadata.rs:L83-L84` (item-doc) + +```text +A "virtual cell" is a PLONK cell that has been queried at a particular +relative offset within a custom gate. +``` + +### `dev/metadata.rs:L132-L133` (item-doc) + +```text +Helper structure used to be able to inject Column annotations inside a +`Display` or `Debug` call. +``` + +### `dev/metadata.rs:L161` (item-doc) + +```text +Metadata about a configured gate within a circuit. +``` + +### `dev/metadata.rs:L164-L166` (item-doc) + +```text +The index of the active gate. These indices are assigned in the order in +which `ConstraintSystem::create_gate` is called during +`Circuit::configure`. +``` + +### `dev/metadata.rs:L168-L169` (item-doc) + +```text +The name of the active gate. These are specified by the gate creator +(such as a chip implementation), and is not enforced to be unique. +``` + +### `dev/metadata.rs:L188` (item-doc) + +```text +Metadata about a configured constraint within a circuit. +``` + +### `dev/metadata.rs:L191` (item-doc) + +```text +The gate containing the constraint. +``` + +### `dev/metadata.rs:L193-L196` (item-doc) + +```text +The index of the polynomial constraint within the gate. These indices +correspond to the order in which the constraints are returned from +the closure passed to `ConstraintSystem::create_gate` during +`Circuit::configure`. +``` + +### `dev/metadata.rs:L198-L199` (item-doc) + +```text +The name of the constraint. This is specified by the gate creator (such +as a chip implementation), and is not enforced to be unique. +``` + +### `dev/metadata.rs:L230` (item-doc) + +```text +Metadata about an assigned region within a circuit. +``` + +### `dev/metadata.rs:L233-L235` (item-doc) + +```text +The index of the region. These indices are assigned in the order in +which `Layouter::assign_region` is called during +`Circuit::synthesize`. +``` + +### `dev/metadata.rs:L237-L238` (item-doc) + +```text +The name of the region. This is specified by the region creator (such as +a chip implementation), and is not enforced to be unique. +``` + +### `dev/metadata.rs:L240-L241` (item-doc) + +```text +A reference to the annotations of the Columns that exist within this +`Region`. +``` + +### `dev/metadata.rs:L246-L252` (item-doc) + +```text +Fetch the annotation of a `Column` within a `Region` providing it's +associated metadata. + +This function will return `None` if: +- There's no annotation map generated for this `Region`. +- There's no entry on the annotation map corresponding to the metadata + provided. +``` + + +## `dev/mod.rs` + +### `dev/mod.rs:L1` (module-doc) + +```text +Tools for developing circuits. +``` + +### `dev/mod.rs:L48` (item-doc) + +```text +The name of the region. Not required to be unique. +``` + +### `dev/mod.rs:L50` (item-doc) + +```text +The columns involved in this region. +``` + +### `dev/mod.rs:L52` (item-doc) + +```text +The rows that this region starts and ends on, if known. +``` + +### `dev/mod.rs:L54-L55` (item-doc) + +```text +The selectors that have been enabled in this region. All other selectors +are by construction not enabled. +``` + +### `dev/mod.rs:L57-L58` (item-doc) + +```text +Annotations given to Advice, Fixed or Instance columns within a region +context. +``` + +### `dev/mod.rs:L60-L61` (item-doc) + +```text +The cells assigned in this region. We store this as a `Vec` so that if +any cells are double-assigned, they will be visibly darker. +``` + +### `dev/mod.rs:L69-L70` (line) + +```text +The region start is the earliest row assigned to. +The region end is the latest row assigned to. +``` + +### `dev/mod.rs:L73` (line) + +```text +The first row assigned was not at start 0 within the region. +``` + +### `dev/mod.rs:L83` (item-doc) + +```text +The value of a particular cell within the circuit. +``` + +### `dev/mod.rs:L86` (item-doc) + +```text +An unassigned cell. +``` + +### `dev/mod.rs:L88` (item-doc) + +```text +A cell that has been assigned a value. +``` + +### `dev/mod.rs:L90` (item-doc) + +```text +A unique poisoned cell. +``` + +### `dev/mod.rs:L94` (item-doc) + +```text +A value within an expression. +``` + +### `dev/mod.rs:L104` (line) + +```text +Cells that haven't been explicitly assigned to, default to zero. +``` + +### `dev/mod.rs:L140-L141` (line) + +```text +If poison is multiplied by zero, then we treat the poison as unconstrained +and we don't propagate it. +``` + +### `dev/mod.rs:L158-L159` (line) + +```text +If poison is multiplied by zero, then we treat the poison as unconstrained +and we don't propagate it. +``` + +### `dev/mod.rs:L166-L281` (item-doc) + +````text +A test prover for debugging circuits. + +The normal proving process, when applied to a buggy circuit implementation, +might return proofs that do not validate when they should, but it can't +indicate anything other than "something is invalid". `MockProver` can be +used to figure out _why_ these are invalid: it stores all the private inputs +along with the circuit internals, and then checks every constraint manually. + +# Examples + +``` +use ff::PrimeField; +use midnight_curves::Fq as Scalar; +use midnight_proofs::{ + circuit::{Layouter, SimpleFloorPlanner, Value}, + dev::{FailureLocation, MockProver, VerifyFailure}, + plonk::{Advice, Any, Circuit, Column, ConstraintSystem, Constraints, Error, Selector}, + poly::Rotation, +}; +const K: u32 = 5; + +#[derive(Copy, Clone)] +struct MyConfig { + a: Column, + b: Column, + c: Column, + s: Selector, +} + +#[derive(Clone, Default)] +struct MyCircuit { + a: Value, + b: Value, +} + +impl Circuit for MyCircuit { + type Config = MyConfig; + type FloorPlanner = SimpleFloorPlanner; + #[cfg(feature = "circuit-params")] + type Params = (); + + fn without_witnesses(&self) -> Self { + Self::default() + } + + fn configure(meta: &mut ConstraintSystem) -> MyConfig { + let a = meta.advice_column(); + let b = meta.advice_column(); + let c = meta.advice_column(); + let s = meta.selector(); + + meta.create_gate("R1CS constraint", |meta| { + let a = meta.query_advice(a, Rotation::cur()); + let b = meta.query_advice(b, Rotation::cur()); + let c = meta.query_advice(c, Rotation::cur()); + + // BUG: Should be a * b - c + Constraints::with_selector(s, vec![("buggy R1CS", (a * b + c))]) + }); + + MyConfig { a, b, c, s } + } + + fn synthesize( + &self, + config: MyConfig, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + layouter.assign_region( + || "Example region", + |mut region| { + config.s.enable(&mut region, 0)?; + region.assign_advice(|| "a", config.a, 0, || self.a.map(F::from))?; + region.assign_advice(|| "b", config.b, 0, || self.b.map(F::from))?; + region.assign_advice(|| "c", config.c, 0, || (self.a * self.b).map(F::from))?; + Ok(()) + }, + ) + } +} + +// Assemble the private inputs to the circuit. +let circuit = MyCircuit { + a: Value::known(2), + b: Value::known(4), +}; + +// This circuit has no public inputs. +let instance = vec![]; + +let prover = MockProver::::run(K, &circuit, instance).unwrap(); +assert_eq!( + prover.verify(), + Err(vec![VerifyFailure::ConstraintNotSatisfied { + constraint: ((0, "R1CS constraint").into(), 0, "buggy R1CS").into(), + location: FailureLocation::InRegion { + region: (0, "Example region").into(), + offset: 0, + }, + cell_values: vec![ + (((Any::advice(), 0).into(), 0).into(), "0x2".to_string()), + (((Any::advice(), 1).into(), 0).into(), "0x4".to_string()), + (((Any::advice(), 2).into(), 0).into(), "0x8".to_string()), + ], + }]) +); + +// If we provide a too-small K, we get a panic. +use std::panic; +let result = + panic::catch_unwind(|| MockProver::::run(2, &circuit, vec![]).unwrap_err()); +assert_eq!( + result.unwrap_err().downcast_ref::().unwrap(), + "n=4, minimum_rows=9, k=2" +); +``` +```` + +### `dev/mod.rs:L288` (item-doc) + +```text +The regions in the circuit. +``` + +### `dev/mod.rs:L290-L291` (item-doc) + +```text +The current region being assigned to. Will be `None` after the circuit +has been synthesized. +``` + +### `dev/mod.rs:L294` (line) + +```text +The fixed cells in the circuit, arranged as [column][row]. +``` + +### `dev/mod.rs:L296` (line) + +```text +The advice cells in the circuit, arranged as [column][row]. +``` + +### `dev/mod.rs:L298` (line) + +```text +The instance cells in the circuit, arranged as [column][row]. +``` + +### `dev/mod.rs:L307` (line) + +```text +A range of available rows for assignment and copies. +``` + +### `dev/mod.rs:L313` (item-doc) + +```text +Instance Value +``` + +### `dev/mod.rs:L316` (item-doc) + +```text +Assigned instance value +``` + +### `dev/mod.rs:L318` (item-doc) + +```text +Padding +``` + +### `dev/mod.rs:L397-L398` (line) + +```text +Track that this selector was enabled. We require that all selectors are +enabled inside some region (i.e. no floating selectors). +``` + +### `dev/mod.rs:L475` (line) + +```text +Propagate `assign` error if the column is in current phase. +``` + +### `dev/mod.rs:L589` (line) + +```text +TODO: Do something with namespaces :) +``` + +### `dev/mod.rs:L593` (line) + +```text +TODO: Do something with namespaces :) +``` + +### `dev/mod.rs:L598-L599` (item-doc) + +```text +Runs a synthetic keygen-and-prove operation on the given circuit, +collecting data about the constraints and their assignments. +``` + +### `dev/mod.rs:L644` (line) + +```text +Fixed columns contain no blinding factors. +``` + +### `dev/mod.rs:L647` (line) + +```text +Advice columns contain blinding factors. +``` + +### `dev/mod.rs:L653` (line) + +```text +Poison unusable rows. +``` + +### `dev/mod.rs:L664` (line) + +```text +Use hash chain to derive deterministic challenges for testing +``` + +### `dev/mod.rs:L715` (item-doc) + +```text +Return the content of an advice column as assigned by the circuit. +``` + +### `dev/mod.rs:L720` (item-doc) + +```text +Return the content of a fixed column as assigned by the circuit. +``` + +### `dev/mod.rs:L725-L727` (item-doc) + +```text +Returns `Ok(())` if this `MockProver` is satisfied, or a list of errors +indicating the reasons that the circuit is not satisfied. +Constraints and lookup are checked at `usable_rows`, parallelly. +``` + +### `dev/mod.rs:L732-L735` (item-doc) + +```text +Returns `Ok(())` if this `MockProver` is satisfied, or a list of errors +indicating the reasons that the circuit is not satisfied. +Constraints are only checked at `gate_row_ids`, and lookup inputs are +only checked at `lookup_input_row_ids`, parallelly. +``` + +### `dev/mod.rs:L746` (line) + +```text +check all the row ids are valid +``` + +### `dev/mod.rs:L758-L759` (line) + +```text +Check that within each region, all cells used in instantiated gates have been +assigned to. +``` + +### `dev/mod.rs:L762` (line) + +```text +Find the gates enabled by this selector +``` + +### `dev/mod.rs:L766-L772` (line) + +```text +Assume that if a queried selector is enabled, the user wants to use the +corresponding gate in some way. + +TODO: This will trip up on the reverse case, where leaving a selector +un-enabled keeps a gate enabled. We could alternatively require that +every selector is explicitly enabled or disabled on every row? But that +seems messy and confusing. +``` + +### `dev/mod.rs:L778` (line) + +```text +Selectors are queried with no rotation. +``` + +### `dev/mod.rs:L784` (line) + +```text +Determine where this cell should have been assigned. +``` + +### `dev/mod.rs:L790-L791` (line) + +```text +Handle instance cells, which are not in the +region. +``` + +### `dev/mod.rs:L808` (line) + +```text +Check that it was assigned! +``` + +### `dev/mod.rs:L844` (line) + +```text +Check that all gates are satisfied for all rows. +``` + +### `dev/mod.rs:L947` (line) + +```text +Check that all lookups exist in their respective tables. +``` + +### `dev/mod.rs:L956-L962` (line) + +```text +We optimize on the basis that the table might have been filled so that the +last usable row now has the fill contents (it doesn't +matter if there was no filling). Note that this "fill +row" necessarily exists in the table, and we use that fact to +slightly simplify the optimization: we're only trying to check that all input +rows are contained in the table, and so we can safely +just drop input rows that match the fill row. +``` + +### `dev/mod.rs:L974-L975` (line) + +```text +In the real prover, the lookup expressions are never enforced on +unusable rows, due to the (1 - (l_last(X) + l_blind(X))) term. +``` + +### `dev/mod.rs:L1008` (line) + +```text +Skip rows where selector is 0 +``` + +### `dev/mod.rs:L1013-L1014` (line) + +```text +Also keep track of the original input row, since we're going +to sort. +``` + +### `dev/mod.rs:L1058` (line) + +```text +Check that permutations preserve the original values of the cells. +``` + +### `dev/mod.rs:L1060` (line) + +```text +Original values of columns involved in the permutation. +``` + +### `dev/mod.rs:L1077` (line) + +```text +Iterate over each column of the permutation +``` + +### `dev/mod.rs:L1079-L1080` (line) + +```text +Iterate over each row of the column to check that the cell's +value is preserved by the mapping. +``` + +### `dev/mod.rs:L1114-L1116` (line) + +```text +Remove any duplicate `ConstraintPoisoned` errors (we check all unavailable +rows in case the trigger is row-specific, but the error message only points +at the constraint). +``` + +### `dev/mod.rs:L1128-L1129` (line) + +```text +Checks if the given expression is guaranteed to be constantly zero at the +given offset. +``` + +### `dev/mod.rs:L1151-L1154` (line) + +```text +Verify that the value of the given cell within the given expression is +irrelevant to the evaluation of the expression. This may be because +the cell is always multiplied by an expression that evaluates to 0, or +because the cell is not being queried in the expression at all. +``` + +### `dev/mod.rs:L1156-L1157` (line) + +```text +Check if a given query (defined by its columnd and rotation, since we +want this function to support different query types) is equal to `cell`. +``` + +### `dev/mod.rs:L1192-L1201` (item-doc) + +````text +Panics if the circuit being checked by this `MockProver` is not +satisfied. + +Any verification failures will be pretty-printed to stderr before the +function panics. + +Apart from the stderr output, this method is equivalent to: +```text +assert_eq!(prover.verify(), Ok(())); +``` +```` + +### `dev/mod.rs:L1212-L1224` (item-doc) + +````text +Panics if the circuit being checked by this `MockProver` is not +satisfied. + +Any verification failures will be pretty-printed to stderr before the +function panics. + +Constraints are only checked at `gate_row_ids`, and lookup inputs are +only checked at `lookup_input_row_ids`, parallelly. + +Apart from the stderr output, this method is equivalent to: +```text +assert_eq!(prover.verify_at_rows(), Ok(())); +``` +```` + +### `dev/mod.rs:L1239` (item-doc) + +```text +Returns the constraint system +``` + +### `dev/mod.rs:L1244` (item-doc) + +```text +Returns the usable rows +``` + +### `dev/mod.rs:L1249-L1250` (item-doc) + +```text +Returns the list of Advice Columns used within a MockProver instance and +the associated values contained on each Cell. +``` + +### `dev/mod.rs:L1255-L1256` (item-doc) + +```text +Returns the list of Fixed Columns used within a MockProver instance and +the associated values contained on each Cell. +``` + +### `dev/mod.rs:L1261-L1262` (item-doc) + +```text +Returns the list of Selector Columns used within a MockProver instance +and the associated values contained on each Cell. +``` + +### `dev/mod.rs:L1267-L1268` (item-doc) + +```text +Returns the list of Instance Columns used within a MockProver instance +and the associated values contained on each Cell. +``` + +### `dev/mod.rs:L1273-L1274` (item-doc) + +```text +Returns the permutation argument (`Assembly`) used within a MockProver +instance. +``` + +### `dev/mod.rs:L1323` (line) + +```text +If q is enabled, a and b must be assigned to. +``` + +### `dev/mod.rs:L1342` (line) + +```text +Enable the equality gate. +``` + +### `dev/mod.rs:L1345` (line) + +```text +Assign a = 0. +``` + +### `dev/mod.rs:L1348` (line) + +```text +Name Column a +``` + +### `dev/mod.rs:L1351` (line) + +```text +Name Column b +``` + +### `dev/mod.rs:L1354-L1356` (line) + +```text +BUG: Forget to assign b = 0! This could go unnoticed during +development, because cell values default to zero, which in this +case is fine, but for other assignments would be broken. +``` + +### `dev/mod.rs:L1418-L1419` (line) + +```text +If q is enabled, a must be in the table. +When q is not enabled, lookup the default value instead. +``` + +### `dev/mod.rs:L1448` (line) + +```text +No assignment needed for the table as is an Instance Column. +``` + +### `dev/mod.rs:L1453` (line) + +```text +Enable the lookup on rows 0 and 1. +``` + +### `dev/mod.rs:L1458` (line) + +```text +Load Advice lookup table with Instance lookup table values. +``` + +### `dev/mod.rs:L1468` (line) + +```text +Assign a = 2 and a = 6. +``` + +### `dev/mod.rs:L1489` (line) + +```text +Enable the lookup on rows 0 and 1. +``` + +### `dev/mod.rs:L1494` (line) + +```text +Load Advice lookup table with Instance lookup table values. +``` + +### `dev/mod.rs:L1504` (line) + +```text +Assign a = 4. +``` + +### `dev/mod.rs:L1512` (line) + +```text +BUG: Assign a = 5, which doesn't exist in the table! +``` + +### `dev/mod.rs:L1531` (line) + +```text +This is our "lookup table". +``` + +### `dev/mod.rs:L1583-L1584` (line) + +```text +If q is enabled, a must be in the table. +When q is not enabled, lookup the default value instead. +``` + +### `dev/mod.rs:L1621` (line) + +```text +Enable the lookup on rows 0 and 1. +``` + +### `dev/mod.rs:L1625` (line) + +```text +Assign a = 2 and a = 6. +``` + +### `dev/mod.rs:L1646` (line) + +```text +Enable the lookup on rows 0 and 1. +``` + +### `dev/mod.rs:L1650` (line) + +```text +Assign a = 4. +``` + +### `dev/mod.rs:L1658` (line) + +```text +BUG: Assign a = 5, which doesn't exist in the table! +``` + +### `dev/mod.rs:L1723` (line) + +```text +If q is enabled, a and b must be assigned to. +``` + +### `dev/mod.rs:L1742` (line) + +```text +Enable the equality gate. +``` + +### `dev/mod.rs:L1745` (line) + +```text +Assign a = 1. +``` + +### `dev/mod.rs:L1748` (line) + +```text +Assign b = 1. +``` + +### `dev/mod.rs:L1751` (line) + +```text +Assign c = 5. +``` + +### `dev/mod.rs:L1758` (line) + +```text +Assign d = 7. +``` + +### `dev/mod.rs:L1771` (line) + +```text +Enable the equality gate. +``` + +### `dev/mod.rs:L1774` (line) + +```text +Assign a = 1. +``` + +### `dev/mod.rs:L1777` (line) + +```text +Assign b = 0. +``` + +### `dev/mod.rs:L1780` (line) + +```text +Name Column a +``` + +### `dev/mod.rs:L1782` (line) + +```text +Name Column b +``` + +### `dev/mod.rs:L1785` (line) + +```text +Assign c = 5. +``` + +### `dev/mod.rs:L1792` (line) + +```text +Assign d = 7. +``` + +### `dev/mod.rs:L1800` (line) + +```text +Name Column c +``` + +### `dev/mod.rs:L1802` (line) + +```text +Name Column d +``` + +### `dev/mod.rs:L1805-L1807` (line) + +```text +Note that none of the terms cancel eachother. Therefore we will have a +constraint that is non satisfied for the `Equalty +check` gate. +``` + + +## `dev/tfp.rs` + +### `dev/tfp.rs:L18-L91` (item-doc) + +````text +A helper type that augments a [`FloorPlanner`] with [`tracing`] spans and +events. + +`TracingFloorPlanner` can be used to instrument your circuit and determine +exactly what is happening during a particular run of keygen or proving. This +can be useful for identifying unexpected non-determinism or changes to a +circuit. + +# No stability guarantees + +The `tracing` output is intended for use during circuit development. It +should not be considered production-stable, and the precise format or data +exposed may change at any time. + +# Examples + +```no_run +use ff::Field; +use midnight_proofs::{ + circuit::{floor_planner, Layouter, Value}, + dev::TracingFloorPlanner, + plonk::{Circuit, ConstraintSystem, Error}, +}; + +# struct MyCircuit { +# some_witness: Value, +# }; +# #[derive(Clone)] +# struct MyConfig; +impl Circuit for MyCircuit { + // Wrap `TracingFloorPlanner` around your existing floor planner of choice. + //type FloorPlanner = floor_planner::V1; + type FloorPlanner = TracingFloorPlanner; + + // The rest of your `Circuit` implementation is unchanged. + type Config = MyConfig; + + #[cfg(feature = "circuit-params")] + type Params = (); + + fn without_witnesses(&self) -> Self { + Self { + some_witness: Value::unknown(), + } + } + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + // .. +# todo!() + } + + fn synthesize( + &self, + config: Self::Config, + layouter: impl Layouter, + ) -> Result<(), Error> { + // .. +# todo!() + } +} + +#[test] +fn some_circuit_test() { + // At the start of your test, enable tracing. + tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) + .with_ansi(false) + .without_time() + .init(); + + // Now when the rest of the test runs, you will get `tracing` output for every + // operation that the circuit performs under the hood! +} +``` +```` + +### `dev/tfp.rs:L113` (item-doc) + +```text +A helper type that augments a [`Circuit`] with [`tracing`] spans and events. +``` + +### `dev/tfp.rs:L157-L158` (item-doc) + +```text +A helper type that augments a [`Layouter`] with [`tracing`] spans and +events. +``` + +### `dev/tfp.rs:L242` (item-doc) + +```text +A helper type that augments a [`Region`] with [`tracing`] spans and events. +``` + +### `dev/tfp.rs:L363-L364` (item-doc) + +```text +A helper type that augments an [`Assignment`] with [`tracing`] spans and +events. +``` + +### `dev/tfp.rs:L510` (line) + +```text +We enter namespace spans in TracingLayouter. +``` + +### `dev/tfp.rs:L516` (line) + +```text +We exit namespace spans in TracingLayouter. +``` + + +## `dev/util.rs` + +### `dev/util.rs:L15` (item-doc) + +```text +Query index +``` + +### `dev/util.rs:L17` (item-doc) + +```text +Column type +``` + +### `dev/util.rs:L19` (item-doc) + +```text +Column index +``` + +### `dev/util.rs:L21` (item-doc) + +```text +Rotation of this query +``` + +### `dev/util.rs:L66` (line) + +```text +Format value as hex. +``` + +### `dev/util.rs:L68` (line) + +```text +Remove leading zeroes. +``` + +### `dev/util.rs:L120` (line) + +```text +None indicates a selector, which we don't bother showing. +``` + + +## `lib.rs` + +### `lib.rs:L1` (module-doc) + +```text +# midnight_proofs +``` + +### `lib.rs:L4` (line) + +```text +The actual lints we want to disable. +``` + + +## `plonk/bench/mod.rs` + +### `plonk/bench/mod.rs:L1` (module-doc) + +```text +Benchmarking utilities for PLONK prover. +``` + + +## `plonk/bench/prover.rs` + +### `plonk/bench/prover.rs:L1` (module-doc) + +```text +Benchmarking utilities for the PLONK prover. +``` + +### `plonk/bench/prover.rs:L26-L33` (item-doc) + +```text +This computes a proof trace for the provided `circuits` when given the +public parameters `params` and the proving key [`ProvingKey`] that was +generated previously for the same circuit. The provided `instances` +are zero-padded internally. + +The trace can then be used to finalise proofs, or to fold them. + +Benchmarks individual internal steps using the provided `group`. +``` + +### `plonk/bench/prover.rs:L44-L46` (line) + +```text +The prover needs to get all instances in non-committed form. However, +the first `nb_committed_instances` instance columns are dedicated for +instances that the verifier receives in committed form. +``` + +### `plonk/bench/prover.rs:L77` (line) + +```text +Hash verification key into transcript +``` + +### `plonk/bench/prover.rs:L126` (line) + +```text +Sample theta challenge for keeping lookup columns linearly independent +``` + +### `plonk/bench/prover.rs:L129` (line) + +```text +Commit to the multiplicities columns +``` + +### `plonk/bench/prover.rs:L189` (line) + +```text +Sample beta challenge +``` + +### `plonk/bench/prover.rs:L192` (line) + +```text +Sample gamma challenge +``` + +### `plonk/bench/prover.rs:L195` (line) + +```text +Commit to permutations +``` + +### `plonk/bench/prover.rs:L243` (line) + +```text +Construct and commit to lookup product polynomials +``` + +### `plonk/bench/prover.rs:L252` (line) + +```text +Construct and commit to products polynomials for each lookup +``` + +### `plonk/bench/prover.rs:L268` (line) + +```text +Construct and commit to products polynomials for each lookup +``` + +### `plonk/bench/prover.rs:L277` (line) + +```text +Trash argument +``` + +### `plonk/bench/prover.rs:L337` (line) + +```text +Obtain challenge for keeping all separate gates linearly independent +``` + +### `plonk/bench/prover.rs:L369-L375` (item-doc) + +```text +This takes the computed trace of a set of witnesses and creates a proof +for the provided `circuit` when given the public +parameters `params` and the proving key [`ProvingKey`] that was +generated previously for the same circuit. The provided `instances` +are zero-padded internally. + +Benchmarks individual internal steps using the provided `group`. +``` + +### `plonk/bench/prover.rs:L379-L381` (line) + +```text +The prover needs to get all instances in non-committed form. However, +the first `nb_committed_instances` instance columns are dedicated for +instances that the verifier receives in committed form. +``` + +### `plonk/bench/prover.rs:L408-L410` (line) + +```text +Construct the quotient polynomial h(X) = nu(X)/(X^n-1) and commit. +When `single-h-commitment` is enabled this produces a single commitment; +otherwise h(X) is split into limbs and each is committed separately. +``` + +### `plonk/bench/prover.rs:L476` (line) + +```text +Evaluate common permutation data +``` + +### `plonk/bench/prover.rs:L488` (line) + +```text +Evaluate the permutations, if any, at omega^i x. +``` + +### `plonk/bench/prover.rs:L494` (line) + +```text +Evaluate the lookups, if any, at omega^i x. +``` + +### `plonk/bench/prover.rs:L505` (line) + +```text +Evaluate the trashcans, if any, at x. +``` + +### `plonk/bench/prover.rs:L516-L517` (line) + +```text +Partially evaluate batched identities (without fixed columns +corresponding to simple, multiplicative selectors) +``` + +### `plonk/bench/prover.rs:L561` (line) + +```text +Compute linearization polynomial +``` + +### `plonk/bench/prover.rs:L625-L629` (item-doc) + +```text +Benchmarked version of proof creation that measures each internal step. + +This function simply calls `compute_trace` and `finalise_proof` with the +provided benchmark group, which causes those functions to benchmark their +internal steps. +``` + + +## `plonk/circuit.rs` + +### `plonk/circuit.rs:L25` (item-doc) + +```text +A column type +``` + +### `plonk/circuit.rs:L29` (item-doc) + +```text +Return expression from cell +``` + +### `plonk/circuit.rs:L33` (item-doc) + +```text +A column with an index and type +``` + +### `plonk/circuit.rs:L46` (item-doc) + +```text +Index of this column. +``` + +### `plonk/circuit.rs:L51` (item-doc) + +```text +Type of this column. +``` + +### `plonk/circuit.rs:L56` (item-doc) + +```text +Return expression from column at a relative position +``` + +### `plonk/circuit.rs:L61` (item-doc) + +```text +Return expression from column at the current row +``` + +### `plonk/circuit.rs:L66` (item-doc) + +```text +Return expression from column at the next row +``` + +### `plonk/circuit.rs:L71` (item-doc) + +```text +Return expression from column at the previous row +``` + +### `plonk/circuit.rs:L76` (item-doc) + +```text +Return expression from column at the specified rotation +``` + +### `plonk/circuit.rs:L84-L85` (line) + +```text +This ordering is consensus-critical! The layouters rely on deterministic +column orderings. +``` + +### `plonk/circuit.rs:L87` (line) + +```text +Indices are assigned within column types. +``` + +### `plonk/circuit.rs:L101` (item-doc) + +```text +Phase of advice column +``` + +### `plonk/circuit.rs:L117` (item-doc) + +```text +Sealed trait to help keep `Phase` private. +``` + +### `plonk/circuit.rs:L123` (item-doc) + +```text +Phase of advice column +``` + +### `plonk/circuit.rs:L128` (item-doc) + +```text +First phase +``` + +### `plonk/circuit.rs:L138` (item-doc) + +```text +Second phase +``` + +### `plonk/circuit.rs:L148` (item-doc) + +```text +Third phase +``` + +### `plonk/circuit.rs:L158` (item-doc) + +```text +An advice column +``` + +### `plonk/circuit.rs:L173` (item-doc) + +```text +Returns `Advice` in given `Phase` +``` + +### `plonk/circuit.rs:L180` (item-doc) + +```text +Phase of this column +``` + +### `plonk/circuit.rs:L189` (line) + +```text +Only show advice's phase if it's not in first phase. +``` + +### `plonk/circuit.rs:L197` (item-doc) + +```text +A fixed column +``` + +### `plonk/circuit.rs:L201` (item-doc) + +```text +An instance column +``` + +### `plonk/circuit.rs:L205` (item-doc) + +```text +An enum over the Advice, Fixed, Instance structs +``` + +### `plonk/circuit.rs:L208` (item-doc) + +```text +An Advice variant +``` + +### `plonk/circuit.rs:L210` (item-doc) + +```text +A Fixed variant +``` + +### `plonk/circuit.rs:L212` (item-doc) + +```text +An Instance variant +``` + +### `plonk/circuit.rs:L217` (item-doc) + +```text +Returns Advice variant in `FirstPhase` +``` + +### `plonk/circuit.rs:L222` (item-doc) + +```text +Returns Advice variant in given `Phase` +``` + +### `plonk/circuit.rs:L233` (line) + +```text +Only show advice's phase if it's not in first phase. +``` + +### `plonk/circuit.rs:L247-L248` (line) + +```text +This ordering is consensus-critical! The layouters rely on deterministic +column orderings. +``` + +### `plonk/circuit.rs:L252` (line) + +```text +Across column types, sort Instance < Advice < Fixed. +``` + +### `plonk/circuit.rs:L407-L462` (item-doc) + +````text +A selector, representing a fixed boolean value per row of the circuit. + +Selectors can be used to conditionally enable (portions of) gates: +``` +use midnight_proofs::poly::Rotation; +# use midnight_curves::Fq as Scalar; +# use midnight_proofs::plonk::{Constraints, ConstraintSystem}; + +# let mut meta = ConstraintSystem::::default(); +let a = meta.advice_column(); +let b = meta.advice_column(); +let s = meta.selector(); + +meta.create_gate("foo", |meta| { + let a = meta.query_advice(a, Rotation::prev()); + let b = meta.query_advice(b, Rotation::cur()); + + // On rows where the selector is enabled, a is constrained to equal b. + // On rows where the selector is disabled, a and b can take any value. + Constraints::with_selector(s, vec![a - b]) +}); +``` + +Selectors are disabled on all rows by default, and must be explicitly +enabled on each row when required: +``` +use ff::Field; +use midnight_proofs::{ + circuit::{Chip, Layouter, Value}, + plonk::{Advice, Column, Error, Selector}, +}; +# use midnight_proofs::plonk::Fixed; + +struct Config { + a: Column, + b: Column, + s: Selector, +} + +fn circuit_logic>( + chip: C, + mut layouter: impl Layouter, +) -> Result<(), Error> { + let config = chip.config(); + # let config: Config = todo!(); + layouter.assign_region( + || "bar", + |mut region| { + region.assign_advice(|| "a", config.a, 0, || Value::known(F::ONE))?; + region.assign_advice(|| "a", config.b, 1, || Value::known(F::ONE))?; + config.s.enable(&mut region, 1) + }, + )?; + Ok(()) +} +``` +```` + +### `plonk/circuit.rs:L467` (item-doc) + +```text +Enable this selector at the given offset within the given region. +``` + +### `plonk/circuit.rs:L472-L473` (item-doc) + +```text +Is this selector "simple"? Simple selectors can only be multiplied +by expressions that contain no other simple selectors. +``` + +### `plonk/circuit.rs:L478` (item-doc) + +```text +Returns index of this selector +``` + +### `plonk/circuit.rs:L483` (item-doc) + +```text +Return expression from selector +``` + +### `plonk/circuit.rs:L489` (item-doc) + +```text +Query of fixed column at a certain relative location +``` + +### `plonk/circuit.rs:L492` (item-doc) + +```text +Query index +``` + +### `plonk/circuit.rs:L494` (item-doc) + +```text +Column index +``` + +### `plonk/circuit.rs:L496` (item-doc) + +```text +Rotation of this query +``` + +### `plonk/circuit.rs:L501` (item-doc) + +```text +Return the query index +``` + +### `plonk/circuit.rs:L505` (item-doc) + +```text +Column index +``` + +### `plonk/circuit.rs:L510` (item-doc) + +```text +Rotation of this query +``` + +### `plonk/circuit.rs:L516` (item-doc) + +```text +Query of advice column at a certain relative location +``` + +### `plonk/circuit.rs:L519` (item-doc) + +```text +Query index +``` + +### `plonk/circuit.rs:L521` (item-doc) + +```text +Column index +``` + +### `plonk/circuit.rs:L523` (item-doc) + +```text +Rotation of this query +``` + +### `plonk/circuit.rs:L525` (item-doc) + +```text +Phase of this advice column +``` + +### `plonk/circuit.rs:L530` (item-doc) + +```text +Column index +``` + +### `plonk/circuit.rs:L535` (item-doc) + +```text +Rotation of this query +``` + +### `plonk/circuit.rs:L540` (item-doc) + +```text +Phase of this advice column +``` + +### `plonk/circuit.rs:L546` (item-doc) + +```text +Query of instance column at a certain relative location +``` + +### `plonk/circuit.rs:L549` (item-doc) + +```text +Query index +``` + +### `plonk/circuit.rs:L551` (item-doc) + +```text +Column index +``` + +### `plonk/circuit.rs:L553` (item-doc) + +```text +Rotation of this query +``` + +### `plonk/circuit.rs:L558` (item-doc) + +```text +Column index +``` + +### `plonk/circuit.rs:L563` (item-doc) + +```text +Rotation of this query +``` + +### `plonk/circuit.rs:L569-L579` (item-doc) + +```text +A fixed column of a lookup table. + +A lookup table can be loaded into this column via +[`Layouter::assign_table`]. Columns can currently only contain a single +table, but they may be used in multiple lookup arguments via +[`ConstraintSystem::lookup`]. + +Lookup table columns are always "encumbered" by the lookup arguments they +are used in; they cannot simultaneously be used as general fixed columns. + +[`Layouter::assign_table`]: crate::circuit::Layouter::assign_table +``` + +### `plonk/circuit.rs:L582-L588` (item-doc) + +```text +The fixed column that this table column is stored in. + +# Security + +This inner column MUST NOT be exposed in the public API, or else chip +developers can load lookup tables into their circuits without +default-value-filling the columns, which can cause soundness bugs. +``` + +### `plonk/circuit.rs:L593` (item-doc) + +```text +Returns inner column +``` + +### `plonk/circuit.rs:L599-L600` (item-doc) + +```text +A challenge squeezed from transcript after advice columns at the phase have +been committed. +``` + +### `plonk/circuit.rs:L608` (item-doc) + +```text +Index of this challenge. +``` + +### `plonk/circuit.rs:L613` (item-doc) + +```text +Phase of this challenge. +``` + +### `plonk/circuit.rs:L618` (item-doc) + +```text +Return Expression +``` + +### `plonk/circuit.rs:L624-L625` (item-doc) + +```text +This trait allows a [`Circuit`] to direct some backend to assign a witness +for a constraint system. +``` + +### `plonk/circuit.rs:L627-L635` (item-doc) + +```text +Creates a new region and enters into it. + +Panics if we are currently in a region (if `exit_region` was not +called). + +Not intended for downstream consumption; use [`Layouter::assign_region`] +instead. + +[`Layouter::assign_region`]: crate::circuit::Layouter#method.assign_region +``` + +### `plonk/circuit.rs:L641-L644` (item-doc) + +```text +Allows the developer to include an annotation for an specific column +within a `Region`. + +This is usually useful for debugging circuit failures. +``` + +### `plonk/circuit.rs:L650-L658` (item-doc) + +```text +Exits the current region. + +Panics if we are not currently in a region (if `enter_region` was not +called). + +Not intended for downstream consumption; use [`Layouter::assign_region`] +instead. + +[`Layouter::assign_region`]: crate::circuit::Layouter#method.assign_region +``` + +### `plonk/circuit.rs:L661` (item-doc) + +```text +Enables a selector at the given row. +``` + +### `plonk/circuit.rs:L672-L674` (item-doc) + +```text +Queries the cell of an instance column at a particular absolute row. + +Returns the cell's value, if known. +``` + +### `plonk/circuit.rs:L677` (item-doc) + +```text +Assign an advice column value (witness) +``` + +### `plonk/circuit.rs:L691` (item-doc) + +```text +Assign a fixed value +``` + +### `plonk/circuit.rs:L705` (item-doc) + +```text +Assign two cells to have the same value +``` + +### `plonk/circuit.rs:L714` (item-doc) + +```text +Fills a fixed `column` starting from the given `row` with value `to`. +``` + +### `plonk/circuit.rs:L722-L725` (item-doc) + +```text +Queries the value of the given challenge. + +Returns `Value::unknown()` if the current synthesis phase is before the +challenge can be queried. +``` + +### `plonk/circuit.rs:L728-L733` (item-doc) + +```text +Creates a new (sub)namespace and enters into it. + +Not intended for downstream consumption; use [`Layouter::namespace`] +instead. + +[`Layouter::namespace`]: crate::circuit::Layouter#method.namespace +``` + +### `plonk/circuit.rs:L739-L744` (item-doc) + +```text +Exits out of the existing namespace. + +Not intended for downstream consumption; use [`Layouter::namespace`] +instead. + +[`Layouter::namespace`]: crate::circuit::Layouter#method.namespace +``` + +### `plonk/circuit.rs:L748-L751` (item-doc) + +```text +A floor planning strategy for a circuit. + +The floor planner is chip-agnostic and applies its strategy to the circuit +it is used within. +``` + +### `plonk/circuit.rs:L753-L764` (item-doc) + +```text +Given the provided `cs`, synthesize the given circuit. + +`constants` is the list of fixed columns that the layouter may use to +assign global constant values. These columns will all have been +equality-enabled. + +Internally, a floor planner will perform the following operations: +- Instantiate a [`Layouter`] for this floor planner. +- Perform any necessary setup or measurement tasks, which may involve + one or more calls to `Circuit::default().synthesize(config, &mut + layouter)`. +- Call `circuit.synthesize(config, &mut layouter)` exactly once. +``` + +### `plonk/circuit.rs:L773-L775` (item-doc) + +```text +This is a trait that circuits provide implementations for so that the +backend prover can ask the circuit to synthesize using some given +[`ConstraintSystem`] implementation. +``` + +### `plonk/circuit.rs:L777` (item-doc) + +```text +This is a configuration object that stores things like columns. +``` + +### `plonk/circuit.rs:L779-L780` (item-doc) + +```text +The floor planner used for this circuit. This is an associated type of +the `Circuit` trait because its behaviour is circuit-critical. +``` + +### `plonk/circuit.rs:L782-L783` (item-doc) + +```text +Optional circuit configuration parameters. Requires the `circuit-params` +feature. +``` + +### `plonk/circuit.rs:L787-L789` (item-doc) + +```text +Returns a copy of this circuit with no witness values (i.e. all +witnesses set to `None`). For most circuits, this will be equal to +`Self::default()`. +``` + +### `plonk/circuit.rs:L792-L793` (item-doc) + +```text +Returns a reference to the parameters that should be used to configure +the circuit. Requires the `circuit-params` feature. +``` + +### `plonk/circuit.rs:L799-L803` (item-doc) + +```text +The circuit is given an opportunity to describe the exact gate +arrangement, column arrangement, etc. Takes a runtime parameter. The +default implementation calls `configure` ignoring the `_params` +argument in order to easily support circuits that don't use +configuration parameters. +``` + +### `plonk/circuit.rs:L812-L813` (item-doc) + +```text +The circuit is given an opportunity to describe the exact gate +arrangement, column arrangement, etc. +``` + +### `plonk/circuit.rs:L816-L818` (item-doc) + +```text +Given the provided `cs`, synthesize the circuit. The concrete type of +the caller will be different depending on the context, and they may or +may not expect to have a witness present. +``` + +### `plonk/circuit.rs:L822-L823` (item-doc) + +```text +Low-degree expression representing an identity that must hold over the +committed columns. +``` + +### `plonk/circuit.rs:L826` (item-doc) + +```text +This is a constant polynomial +``` + +### `plonk/circuit.rs:L828` (item-doc) + +```text +This is a virtual selector +``` + +### `plonk/circuit.rs:L830` (item-doc) + +```text +This is a fixed column queried at a certain relative location +``` + +### `plonk/circuit.rs:L832-L833` (item-doc) + +```text +This is an advice (witness) column queried at a certain relative +location +``` + +### `plonk/circuit.rs:L835-L836` (item-doc) + +```text +This is an instance (external) column queried at a certain relative +location +``` + +### `plonk/circuit.rs:L838` (item-doc) + +```text +This is a challenge +``` + +### `plonk/circuit.rs:L840` (item-doc) + +```text +This is a negated polynomial +``` + +### `plonk/circuit.rs:L842` (item-doc) + +```text +This is the sum of two polynomials +``` + +### `plonk/circuit.rs:L844` (item-doc) + +```text +This is the product of two polynomials +``` + +### `plonk/circuit.rs:L846` (item-doc) + +```text +This is a scaled polynomial +``` + +### `plonk/circuit.rs:L851` (item-doc) + +```text +Make side effects +``` + +### `plonk/circuit.rs:L904-L905` (item-doc) + +```text +Evaluate the polynomial using the provided closures to perform the +operations. +``` + +### `plonk/circuit.rs:L1014-L1015` (item-doc) + +```text +Evaluate the polynomial lazily using the provided closures to perform +the operations. +``` + +### `plonk/circuit.rs:L1195-L1198` (item-doc) + +```text +Identifier for this expression. Expressions with identical identifiers +do the same calculation (but the expressions don't need to be exactly +equal in how they are composed e.g. `1 + 2` and `2 + 1` can have the +same identifier). +``` + +### `plonk/circuit.rs:L1205` (item-doc) + +```text +Compute the degree of this polynomial +``` + +### `plonk/circuit.rs:L1221` (item-doc) + +```text +Approximate the computational complexity of this expression. +``` + +### `plonk/circuit.rs:L1237` (item-doc) + +```text +Square this expression. +``` + +### `plonk/circuit.rs:L1242` (item-doc) + +```text +Returns whether or not this expression contains a simple `Selector`. +``` + +### `plonk/circuit.rs:L1264-L1265` (line) + +```text +Skip enum variant and print query struct directly to maintain backwards +compatibility. +``` + +### `plonk/circuit.rs:L1286` (line) + +```text +Only show advice's phase if it's not in first phase. +``` + +### `plonk/circuit.rs:L1476-L1477` (item-doc) + +```text +A "virtual cell" is a PLONK cell that has been queried at a particular +relative offset within a custom gate. +``` + +### `plonk/circuit.rs:L1493-L1496` (item-doc) + +```text +An individual polynomial constraint. + +These are returned by the closures passed to +`ConstraintSystem::create_gate`. +``` + +### `plonk/circuit.rs:L1537-L1570` (item-doc) + +````text +A set of polynomial constraints with a common selector. + +``` +use ff::Field; +use midnight_curves::Fq as Scalar; +use midnight_proofs::{ + plonk::{Constraints, Expression}, + poly::Rotation, +}; +# use midnight_proofs::plonk::ConstraintSystem; + +# let mut meta = ConstraintSystem::::default(); +let a = meta.advice_column(); +let b = meta.advice_column(); +let c = meta.advice_column(); +let s_ternary = meta.selector(); + +meta.create_gate("foo", |meta| { + let next = meta.query_advice(a, Rotation::next()); + let a = meta.query_advice(a, Rotation::cur()); + let b = meta.query_advice(b, Rotation::cur()); + let c = meta.query_advice(c, Rotation::cur()); + + let one_minus_a = Expression::Constant(Scalar::ONE) - a.clone(); + + Constraints::with_selector( + s_ternary, + vec![ + ("a is boolean", a.clone() * one_minus_a.clone()), + ("next == a ? b : c", next - (a * b + one_minus_a * c)), + ], + ) +}); +``` +```` + +### `plonk/circuit.rs:L1578-L1582` (item-doc) + +```text +Constructs a set of constraints that are controlled by the given +selector. + +Each constraint `c` in `iterator` will be converted into the constraint +`selector * c`. +``` + +### `plonk/circuit.rs:L1590-L1596` (item-doc) + +```text +Constructs a set of constraints that are controlled by the given +selector. The selector is involved additively (via a trash::argument) +and thus, it does not increase the constraints degree. + +The enforced constraint will have the form +`(sum_i r^i constraints[i]) - (1 - selector) * trash`, where `r` is +a Fiat-Shamir challenge and `trash` is an unrestricted column. +``` + +### `plonk/circuit.rs:L1607-L1608` (item-doc) + +```text +Constructs a set of constraints that do not have a selector and +therefore are always enabled. +``` + +### `plonk/circuit.rs:L1617` (item-doc) + +```text +Gate +``` + +### `plonk/circuit.rs:L1623-L1624` (item-doc) + +```text +We track queried selectors separately from other cells, so that we can +use them to trigger debug checks on gates. +``` + +### `plonk/circuit.rs:L1630` (item-doc) + +```text +Returns the gate name. +``` + +### `plonk/circuit.rs:L1635` (item-doc) + +```text +Returns the name of the constraint at index `constraint_index`. +``` + +### `plonk/circuit.rs:L1640` (item-doc) + +```text +Returns constraints of this gate +``` + +### `plonk/circuit.rs:L1645` (item-doc) + +```text +Returns the queried selectors of this gate. +``` + +### `plonk/circuit.rs:L1655` (item-doc) + +```text +Type for tracking information about selectors. +``` + +### `plonk/circuit.rs:L1660-L1661` (item-doc) + +```text +Returns `true` if this selector flag tracks a simple selector, `false` +otherwise. +``` + +### `plonk/circuit.rs:L1666-L1669` (item-doc) + +```text +Returns an [Option] containing + * the column index of this selector after it has been converted to a + fixed column, + * `None` otherwise. +``` + +### `plonk/circuit.rs:L1675-L1676` (item-doc) + +```text +This is a description of the circuit environment, such as the gate, column +and permutation arrangements. +``` + +### `plonk/circuit.rs:L1686` (item-doc) + +```text +Contains the index of each advice column that is left unblinded. +``` + +### `plonk/circuit.rs:L1689-L1690` (item-doc) + +```text +Contains the phase for each advice column. Should have same length as +num_advice_columns. +``` + +### `plonk/circuit.rs:L1692-L1693` (item-doc) + +```text +Contains the phase for each challenge. Should have same length as +num_challenges. +``` + +### `plonk/circuit.rs:L1698-L1700` (line) + +```text +Contains an integer for each advice column +identifying how many distinct queries it has +so far; should be same length as num_advice_columns. +``` + +### `plonk/circuit.rs:L1705` (line) + +```text +Permutation argument for performing equality constraints +``` + +### `plonk/circuit.rs:L1708-L1709` (line) + +```text +Vector of lookup arguments, where each corresponds to a sequence of +input expressions and a sequence of table expressions involved in the lookup. +``` + +### `plonk/circuit.rs:L1712-L1715` (line) + +```text +Vector of trash arguments. Each contains a selector and a sequence of expressions +that will all be enforced to be zero when the selector is enabled. This is done +via an argument that involves the selector additively (instead of multiplicatively) +with the help of an extra so-called trash column. +``` + +### `plonk/circuit.rs:L1718-L1719` (line) + +```text +List of indexes of Fixed columns which are associated to a circuit-general Column tied to +their annotation. +``` + +### `plonk/circuit.rs:L1722-L1723` (line) + +```text +Vector of fixed columns, which can be used to store constant values +that are copied into advice columns. +``` + +### `plonk/circuit.rs:L1729` (item-doc) + +```text +Represents the minimal parameters that determine a `ConstraintSystem`. +``` + +### `plonk/circuit.rs:L1776` (line) + +```text +Only show multi-phase related fields if it's used. +``` + +### `plonk/circuit.rs:L1836-L1837` (item-doc) + +```text +Returns `true` if this constraint system contains a [SelectorFlag] of a +simple selector with the given column index. +``` + +### `plonk/circuit.rs:L1844` (item-doc) + +```text +Returns the number of [SelectorFlag]s that track simple selectors. +``` + +### `plonk/circuit.rs:L1849-L1851` (item-doc) + +```text +Obtain a pinned version of this constraint system; a structure with the +minimal parameters needed to determine the rest of the constraint +system. +``` + +### `plonk/circuit.rs:L1873-L1877` (item-doc) + +```text +Enables this fixed column to be used for global constant assignments. + +# Side-effects + +The column will be equality-enabled. +``` + +### `plonk/circuit.rs:L1885` (item-doc) + +```text +Enable the ability to enforce equality over cells in this column +``` + +### `plonk/circuit.rs:L1892-L1898` (item-doc) + +```text +Add a lookup argument for a single input expression per table column. + +`table_map` returns a vector of maps between an input expression and the +table column it needs to match. + +If you want to batch multiple lookups to the same table column in +parallel, use [`batched_lookup`](Self::batched_lookup) instead. +``` + +### `plonk/circuit.rs:L1910-L1914` (item-doc) + +```text +Add a lookup argument for batches of input expressions to a table +column. + +`table_map` returns a vector of maps between input expressions +and the table columns they need to match. +``` + +### `plonk/circuit.rs:L1949-L1956` (item-doc) + +```text +Add a lookup argument for a single input expression per table +expression. + +`table_map` returns a vector of maps between an input expression and the +table expression it needs to match. + +If you want to batch multiple lookups to the same table expression in +parallel, use [`batch_lookup_any`](Self::batch_lookup_any) instead. +``` + +### `plonk/circuit.rs:L1968-L1972` (item-doc) + +```text +Add a lookup argument for batches of input expressions and table +expressions. + +`table_map` returns a vector of maps between input expressions (batched +together) and the table expressions they need to match. +``` + +### `plonk/circuit.rs:L2010` (line) + +```text +Return existing query, if it exists +``` + +### `plonk/circuit.rs:L2017` (line) + +```text +Make a new query +``` + +### `plonk/circuit.rs:L2025` (line) + +```text +Return existing query, if it exists +``` + +### `plonk/circuit.rs:L2032` (line) + +```text +Make a new query +``` + +### `plonk/circuit.rs:L2041` (line) + +```text +Return existing query, if it exists +``` + +### `plonk/circuit.rs:L2048` (line) + +```text +Make a new query +``` + +### `plonk/circuit.rs:L2111-L2113` (item-doc) + +```text +Sets the minimum degree required by the circuit, which can be set to a +larger amount than actually needed. This can be used, for example, to +force the permutation argument to involve more columns in the same set. +``` + +### `plonk/circuit.rs:L2118-L2123` (item-doc) + +```text +Creates a new gate. + +# Panics + +If `constraints` returns an empty iterator. +(Gates are required to contain polynomial constraints.) +``` + +### `plonk/circuit.rs:L2158-L2159` (item-doc) + +```text +Does not combine selectors and directly replaces them everywhere with +fixed columns. +``` + +### `plonk/circuit.rs:L2164-L2165` (line) + +```text +The number of provided selector assignments must be the number we +counted for this constraint system. +``` + +### `plonk/circuit.rs:L2192-L2194` (line) + +```text +Adjust indices of simple, multiplicative selectors: after converting +selectors to fixed columns, the selector index of a gate should now +track the index of the corresponding fixed column +``` + +### `plonk/circuit.rs:L2214-L2215` (line) + +```text +Simple selectors are prohibited from appearing in +expressions in the lookup or trash arguments. +``` + +### `plonk/circuit.rs:L2232` (line) + +```text +Substitute selectors for the real fixed columns in all gates. +``` + +### `plonk/circuit.rs:L2237-L2238` (line) + +```text +Substitute non-simple selectors for the real fixed columns in all +lookup expressions. +``` + +### `plonk/circuit.rs:L2249` (line) + +```text +Substitute selectors in the lookup selector fields. +``` + +### `plonk/circuit.rs:L2254` (line) + +```text +Substitute selectors in all trash arguments. +``` + +### `plonk/circuit.rs:L2263-L2266` (item-doc) + +```text +Allocate a new (simple) selector. Simple selectors cannot be added to +expressions nor multiplied by other expressions containing simple +selectors. Also, simple selectors may not appear in lookup argument +inputs. +``` + +### `plonk/circuit.rs:L2274-L2275` (item-doc) + +```text +Allocate a new complex selector that can appear anywhere +within expressions. +``` + +### `plonk/circuit.rs:L2283` (item-doc) + +```text +Allocates a new fixed column that can be used in a lookup table. +``` + +### `plonk/circuit.rs:L2290` (item-doc) + +```text +Annotate a Lookup column. +``` + +### `plonk/circuit.rs:L2296-L2297` (line) + +```text +We don't care if the table has already an annotation. If it's the case we +keep the new one. +``` + +### `plonk/circuit.rs:L2304` (item-doc) + +```text +Annotate an Instance column. +``` + +### `plonk/circuit.rs:L2312-L2313` (line) + +```text +We don't care if the table has already an annotation. If it's the case we +keep the new one. +``` + +### `plonk/circuit.rs:L2320` (item-doc) + +```text +Allocate a new fixed column +``` + +### `plonk/circuit.rs:L2330` (item-doc) + +```text +Allocate a new unblinded advice column at `FirstPhase` +``` + +### `plonk/circuit.rs:L2335` (item-doc) + +```text +Allocate a new advice column at `FirstPhase` +``` + +### `plonk/circuit.rs:L2340-L2344` (item-doc) + +```text +Allocate a new unblinded advice column in given phase. This allows for +the generation of deterministic commitments to advice columns +which can be used to split large circuits into smaller ones, whose +proofs can then be "joined" together by their common witness +commitments. +``` + +### `plonk/circuit.rs:L2365-L2369` (item-doc) + +```text +Allocate a new advice column in given phase + +# Panics + +If the previous phase does not have advice columns allocated. +``` + +### `plonk/circuit.rs:L2389` (item-doc) + +```text +Allocate a new instance column +``` + +### `plonk/circuit.rs:L2399-L2403` (item-doc) + +```text +Requests a challenge that is usable after the given phase. + +# Panics + +If the given phase does not have advice column allocated. +``` + +### `plonk/circuit.rs:L2420-L2422` (item-doc) + +```text +Helper funciotn to assert phase exists, to make sure phase-aware +resources are allocated in order, and to avoid any phase to be +skipped accidentally to cause unexpected issue in the future. +``` + +### `plonk/circuit.rs:L2434` (item-doc) + +```text +Return an iterator over the phases of the constraint system. +``` + +### `plonk/circuit.rs:L2441-L2442` (item-doc) + +```text +Compute the degree of the constraint system (the maximum degree of all +constraints). +``` + +### `plonk/circuit.rs:L2458-L2459` (line) + +```text +Logup may increase the degree as long as it does not go to the next +power of two. +``` + +### `plonk/circuit.rs:L2470-L2471` (item-doc) + +```text +Compute the number of blinding factors necessary to perfectly blind +each of the prover's witness polynomials. +``` + +### `plonk/circuit.rs:L2473` (line) + +```text +All of the prover's advice columns are evaluated at no more than +``` + +### `plonk/circuit.rs:L2475` (line) + +```text +distinct points during gate checks. +``` + +### `plonk/circuit.rs:L2477-L2479` (line) + +```text +- The permutation argument witness polynomials are evaluated at most 3 times. +- Each lookup argument has independent witness polynomials, and they are + evaluated at most 2 times. +``` + +### `plonk/circuit.rs:L2482-L2483` (line) + +```text +Each trash argument gives away 1 evaluation of a linear combination of some +witness polynomials (the evaluation of the trash column). +``` + +### `plonk/circuit.rs:L2486-L2490` (line) + +```text +The LogUp helper polynomials are evaluated at x and they are not blinded, so +they may leak information about the witness columns that they involve. +To account for this, we can increase the number of blinding factors of +witness columns. One blinding factor per helper is enough as each helper is +evaluated only once. +``` + +### `plonk/circuit.rs:L2494-L2495` (line) + +```text +Each polynomial is evaluated at most an additional time during +multiopen (at x_3 to produce q_evals): +``` + +### `plonk/circuit.rs:L2498-L2499` (line) + +```text +h(x) is derived by the other evaluations so it does not reveal +anything; in fact it does not even appear in the proof. +``` + +### `plonk/circuit.rs:L2501-L2502` (line) + +```text +Add an additional blinding factor as a slight defense against +off-by-one errors. +``` + +### `plonk/circuit.rs:L2505-L2506` (line) + +```text +Add an additional blinding factor as a security margin for opening +the linearization polynomial in the multi-open argument +``` + +### `plonk/circuit.rs:L2515-L2516` (item-doc) + +```text +Returns the minimum necessary rows that need to exist in order to +account for e.g. blinding factors. +``` + +### `plonk/circuit.rs:L2521-L2523` (line) + +```text +argument, to essentially force a separation in the +permutation polynomial between the roles of l_last, l_0 +and the interstitial values.) +``` + +### `plonk/circuit.rs:L2527` (item-doc) + +```text +Returns number of fixed columns +``` + +### `plonk/circuit.rs:L2532` (item-doc) + +```text +Returns number of advice columns +``` + +### `plonk/circuit.rs:L2537` (item-doc) + +```text +Returns number of instance columns +``` + +### `plonk/circuit.rs:L2542` (item-doc) + +```text +Returns number of selectors +``` + +### `plonk/circuit.rs:L2547` (item-doc) + +```text +Returns number of challenges +``` + +### `plonk/circuit.rs:L2552` (item-doc) + +```text +Returns phase of advice columns +``` + +### `plonk/circuit.rs:L2557` (item-doc) + +```text +Returns phase of challenges +``` + +### `plonk/circuit.rs:L2562` (item-doc) + +```text +Returns gates +``` + +### `plonk/circuit.rs:L2567` (item-doc) + +```text +Returns general column annotations +``` + +### `plonk/circuit.rs:L2572` (item-doc) + +```text +Returns advice queries +``` + +### `plonk/circuit.rs:L2577` (item-doc) + +```text +Returns instance queries +``` + +### `plonk/circuit.rs:L2582` (item-doc) + +```text +Returns fixed queries +``` + +### `plonk/circuit.rs:L2587` (item-doc) + +```text +Returns permutation argument +``` + +### `plonk/circuit.rs:L2592` (item-doc) + +```text +Returns lookup arguments +``` + +### `plonk/circuit.rs:L2597` (item-doc) + +```text +Returns trash arguments +``` + +### `plonk/circuit.rs:L2602` (item-doc) + +```text +Returns constants +``` + +### `plonk/circuit.rs:L2608-L2609` (item-doc) + +```text +Exposes the "virtual cells" that can be queried while creating a custom gate +or lookup table. +``` + +### `plonk/circuit.rs:L2626` (item-doc) + +```text +Query a selector at the current position. +``` + +### `plonk/circuit.rs:L2632` (item-doc) + +```text +Query a fixed column at a relative position +``` + +### `plonk/circuit.rs:L2642` (item-doc) + +```text +Query an advice column at a relative position +``` + +### `plonk/circuit.rs:L2653` (item-doc) + +```text +Query an instance column at a relative position +``` + +### `plonk/circuit.rs:L2663` (item-doc) + +```text +Query an Any column at a relative position +``` + +### `plonk/circuit.rs:L2673` (item-doc) + +```text +Query a challenge +``` + + +## `plonk/error.rs` + +### `plonk/error.rs:L5` (item-doc) + +```text +This is an error that could occur during proving or circuit synthesis. +``` + +### `plonk/error.rs:L6` (line) + +```text +TODO: these errors need to be cleaned up +``` + +### `plonk/error.rs:L9-L10` (item-doc) + +```text +This is an error that can occur during synthesis of the circuit, for +example, when the witness is not present. +``` + +### `plonk/error.rs:L12` (item-doc) + +```text +The provided instances do not match the circuit parameters. +``` + +### `plonk/error.rs:L14` (item-doc) + +```text +The constraint system is not satisfied. +``` + +### `plonk/error.rs:L16` (item-doc) + +```text +Out of bounds index passed to a backend +``` + +### `plonk/error.rs:L18` (item-doc) + +```text +Opening error +``` + +### `plonk/error.rs:L20` (item-doc) + +```text +Transcript error +``` + +### `plonk/error.rs:L22` (item-doc) + +```text +`k` is too small for the given circuit. +``` + +### `plonk/error.rs:L24` (item-doc) + +```text +The current value of `k` being used. +``` + +### `plonk/error.rs:L27` (item-doc) + +```text +Instance provided exceeds number of available rows +``` + +### `plonk/error.rs:L29-L33` (item-doc) + +```text +Circuit synthesis requires global constants, but circuit configuration +did not call [`ConstraintSystem::enable_constant`] on fixed columns +with sufficient space. + +[`ConstraintSystem::enable_constant`]: crate::plonk::ConstraintSystem::enable_constant +``` + +### `plonk/error.rs:L35-L36` (item-doc) + +```text +The instance sets up a copy constraint involving a column that has not +been included in the permutation. +``` + +### `plonk/error.rs:L38` (item-doc) + +```text +An error relating to a lookup table. +``` + +### `plonk/error.rs:L40` (item-doc) + +```text +The SRS provided does not have the correct size for the Circuit +``` + +### `plonk/error.rs:L46` (line) + +```text +The only place we can get io::Error from is the transcript. +``` + +### `plonk/error.rs:L52` (item-doc) + +```text +Constructs an `Error::NotEnoughRowsAvailable`. +``` + +### `plonk/error.rs:L97` (item-doc) + +```text +This is an error that could occur during table synthesis. +``` + +### `plonk/error.rs:L100` (item-doc) + +```text +A `TableColumn` has not been assigned. +``` + +### `plonk/error.rs:L102` (item-doc) + +```text +A Table has columns of uneven lengths. +``` + +### `plonk/error.rs:L104` (item-doc) + +```text +Attempt to assign a used `TableColumn` +``` + +### `plonk/error.rs:L106` (item-doc) + +```text +Attempt to overwrite a default value +``` + + +## `plonk/evaluation.rs` + +### `plonk/evaluation.rs:L11` (item-doc) + +```text +Return the index in the polynomial of size `isize` after rotation `rot`. +``` + +### `plonk/evaluation.rs:L16` (item-doc) + +```text +Value used in a calculation +``` + +### `plonk/evaluation.rs:L19` (item-doc) + +```text +This is a constant value +``` + +### `plonk/evaluation.rs:L21` (item-doc) + +```text +This is an intermediate value +``` + +### `plonk/evaluation.rs:L23` (item-doc) + +```text +This is a fixed column +``` + +### `plonk/evaluation.rs:L25` (item-doc) + +```text +This is an advice (witness) column +``` + +### `plonk/evaluation.rs:L27` (item-doc) + +```text +This is an instance (external) column +``` + +### `plonk/evaluation.rs:L29` (item-doc) + +```text +This is a challenge +``` + +### `plonk/evaluation.rs:L31` (item-doc) + +```text +beta +``` + +### `plonk/evaluation.rs:L33` (item-doc) + +```text +theta +``` + +### `plonk/evaluation.rs:L35` (item-doc) + +```text +trash challenge +``` + +### `plonk/evaluation.rs:L37` (item-doc) + +```text +y +``` + +### `plonk/evaluation.rs:L39` (item-doc) + +```text +Previous value +``` + +### `plonk/evaluation.rs:L50` (item-doc) + +```text +Get the value for this source +``` + +### `plonk/evaluation.rs:L89` (item-doc) + +```text +Calculation +``` + +### `plonk/evaluation.rs:L92` (item-doc) + +```text +This is an addition +``` + +### `plonk/evaluation.rs:L94` (item-doc) + +```text +This is a subtraction +``` + +### `plonk/evaluation.rs:L96` (item-doc) + +```text +This is a product +``` + +### `plonk/evaluation.rs:L98` (item-doc) + +```text +This is a square +``` + +### `plonk/evaluation.rs:L100` (item-doc) + +```text +This is a double +``` + +### `plonk/evaluation.rs:L102` (item-doc) + +```text +This is a negation +``` + +### `plonk/evaluation.rs:L104` (item-doc) + +```text +This is Horner's rule: `val = a; val = val * c + b[]` +``` + +### `plonk/evaluation.rs:L106` (item-doc) + +```text +This is a simple assignment +``` + +### `plonk/evaluation.rs:L111` (item-doc) + +```text +Get the resulting value of this calculation +``` + +### `plonk/evaluation.rs:L164-L165` (item-doc) + +```text +Wraps a `GraphEvaluator` for lookups with named handles to the evaluator +outputs. +``` + +### `plonk/evaluation.rs:L168` (item-doc) + +```text +The underlying computation graph +``` + +### `plonk/evaluation.rs:L170` (item-doc) + +```text +Value containing the sum of partial products, Σⱼ âˆ_{k≠j}(fâ‚– + β) +``` + +### `plonk/evaluation.rs:L172` (item-doc) + +```text +Value containing the product, âˆâ±¼(fâ±¼ + β) +``` + +### `plonk/evaluation.rs:L174` (item-doc) + +```text +Value containing the compressed table value (t + β) +``` + +### `plonk/evaluation.rs:L176` (item-doc) + +```text +Selector of the lookup argument +``` + +### `plonk/evaluation.rs:L180` (item-doc) + +```text +Evaluator +``` + +### `plonk/evaluation.rs:L183` (item-doc) + +```text + Custom gates evaluation +``` + +### `plonk/evaluation.rs:L185-L186` (item-doc) + +```text + Lookups evaluation (one Vec per BatchedArgument, one entry per +flattened arg) +``` + +### `plonk/evaluation.rs:L188` (item-doc) + +```text + Trashcans evaluation +``` + +### `plonk/evaluation.rs:L192` (item-doc) + +```text +GraphEvaluator +``` + +### `plonk/evaluation.rs:L195` (item-doc) + +```text +Constants +``` + +### `plonk/evaluation.rs:L197` (item-doc) + +```text +Rotations +``` + +### `plonk/evaluation.rs:L199` (item-doc) + +```text +Calculations +``` + +### `plonk/evaluation.rs:L201` (item-doc) + +```text +Number of intermediates +``` + +### `plonk/evaluation.rs:L205` (item-doc) + +```text +EvaluationData +``` + +### `plonk/evaluation.rs:L208` (item-doc) + +```text +Intermediates +``` + +### `plonk/evaluation.rs:L210` (item-doc) + +```text +Rotations +``` + +### `plonk/evaluation.rs:L215` (item-doc) + +```text +Resolve a `ValueSource::Intermediate` handle to its computed value. +``` + +### `plonk/evaluation.rs:L224` (item-doc) + +```text +CalculationInfo +``` + +### `plonk/evaluation.rs:L227` (item-doc) + +```text +Calculation +``` + +### `plonk/evaluation.rs:L229` (item-doc) + +```text +Target +``` + +### `plonk/evaluation.rs:L234` (item-doc) + +```text +Creates a new evaluation structure +``` + +### `plonk/evaluation.rs:L238` (line) + +```text +Custom gates +``` + +### `plonk/evaluation.rs:L250` (line) + +```text +Lookups +``` + +### `plonk/evaluation.rs:L259` (line) + +```text +Each input expression gets compressed with θ and shifted by β +``` + +### `plonk/evaluation.rs:L303` (line) + +```text +Compute âˆâ±¼(fâ±¼ + β) and Σⱼ âˆ_{k≠j}(fâ‚– + β) +``` + +### `plonk/evaluation.rs:L314` (line) + +```text +Add β: compressed_table + β +``` + +### `plonk/evaluation.rs:L336` (line) + +```text +Trashcans +``` + +### `plonk/evaluation.rs:L358-L359` (item-doc) + +```text +Evaluate numerator polynomial `nu(X)` of the quotient polynomial +`h(X) = nu(X) / (X^n-1)` +``` + +### `plonk/evaluation.rs:L392` (line) + +```text +Core expression evaluations +``` + +### `plonk/evaluation.rs:L401` (line) + +```text +Custom gates +``` + +### `plonk/evaluation.rs:L430` (line) + +```text +Permutations +``` + +### `plonk/evaluation.rs:L447` (line) + +```text +Permutation constraints +``` + +### `plonk/evaluation.rs:L455-L456` (line) + +```text +Enforce only for the first set. +l_0(X) * (1 - z_0(X)) = 0 +``` + +### `plonk/evaluation.rs:L459-L460` (line) + +```text +Enforce only for the last set. +l_last(X) * (z_l(X)^2 - z_l(X)) = 0 +``` + +### `plonk/evaluation.rs:L466-L467` (line) + +```text +Except for the first set, enforce. +l_0(X) * (z_i(X) - z_{i-1}(\omega^(last) X)) = 0 +``` + +### `plonk/evaluation.rs:L476-L480` (line) + +```text +And for all the sets we enforce: +(1 - (l_last(X) + l_blind(X))) * ( + z_i(\omega X) \prod_j (p(X) + \beta s_j(X) + \gamma) +- z_i(X) \prod_j (p(X) + \delta^j \beta X + \gamma) +) +``` + +### `plonk/evaluation.rs:L518` (line) + +```text +Lookups +``` + +### `plonk/evaluation.rs:L520-L522` (line) + +```text +Polynomials required for this lookup. +Calculated here so these only have to be kept in memory for the short time +they are actually needed. +``` + +### `plonk/evaluation.rs:L531` (line) + +```text +Lookup constraints +``` + +### `plonk/evaluation.rs:L538` (line) + +```text +(l_0(X) + l_last(X)) * Z(X) = 0 +``` + +### `plonk/evaluation.rs:L566` (line) + +```text +We only resolve the table and selector in the first batch +``` + +### `plonk/evaluation.rs:L572` (line) + +```text +Helper constraint: h(X) · âˆâ±¼(fâ±¼(X) + β) = Σⱼ âˆ_{k≠j}(fâ‚–(X) + β) +``` + +### `plonk/evaluation.rs:L579-L580` (line) + +```text +Accumulator constraint: +(Z(ωX) - Z(X)- s·Σᵢháµ¢(X))·(t(X) + β) + m(X) = 0 +``` + +### `plonk/evaluation.rs:L592` (line) + +```text +Trashcans +``` + +### `plonk/evaluation.rs:L594-L596` (line) + +```text +Polynomials required for this trash argument. +Calculated here so these only have to be kept in memory for the short time +they are actually needed. +``` + +### `plonk/evaluation.rs:L599` (line) + +```text +Trash argument constraints. +``` + +### `plonk/evaluation.rs:L628` (line) + +```text +compressed_expressions - (1 - q) * trash +``` + +### `plonk/evaluation.rs:L641` (line) + +```text +Fixed positions to allow easy access +``` + +### `plonk/evaluation.rs:L651` (item-doc) + +```text +Adds a rotation +``` + +### `plonk/evaluation.rs:L663` (item-doc) + +```text +Adds a constant +``` + +### `plonk/evaluation.rs:L675-L678` (item-doc) + +```text +Adds a calculation. +Currently does the simplest thing possible: just stores the +resulting value so the result can be reused when that calculation +is done multiple times. +``` + +### `plonk/evaluation.rs:L695` (item-doc) + +```text +Generates an optimized evaluation for the expression +``` + +### `plonk/evaluation.rs:L735` (line) + +```text +Undo subtraction stored as a + (-b) in expressions +``` + +### `plonk/evaluation.rs:L798` (item-doc) + +```text +Creates a new evaluation structure +``` + +### `plonk/evaluation.rs:L823` (line) + +```text +All rotation index values +``` + +### `plonk/evaluation.rs:L828` (line) + +```text +All calculations, with cached intermediate results +``` + +### `plonk/evaluation.rs:L846` (line) + +```text +Return the result of the last calculation (if any) +``` + +### `plonk/evaluation.rs:L855` (item-doc) + +```text +Simple evaluation of an expression +``` + + +## `plonk/keygen.rs` + +### `plonk/keygen.rs:L52` (item-doc) + +```text +Assembly to be used in circuit synthesis. +``` + +### `plonk/keygen.rs:L59` (line) + +```text +A range of available rows for assignment and copies. +``` + +### `plonk/keygen.rs:L70` (line) + +```text +Do nothing; we don't care about regions in this context. +``` + +### `plonk/keygen.rs:L74` (line) + +```text +Do nothing; we don't care about regions in this context. +``` + +### `plonk/keygen.rs:L96` (line) + +```text +There is no instance in this context. +``` + +### `plonk/keygen.rs:L113` (line) + +```text +We only care about fixed columns here +``` + +### `plonk/keygen.rs:L186` (line) + +```text +Do nothing +``` + +### `plonk/keygen.rs:L194` (line) + +```text +Do nothing; we don't care about namespaces in this context. +``` + +### `plonk/keygen.rs:L198` (line) + +```text +Do nothing; we don't care about namespaces in this context. +``` + +### `plonk/keygen.rs:L202` (item-doc) + +```text +Compute the minimal `k` to compute a circuit. +``` + +### `plonk/keygen.rs:L207-L211` (item-doc) + +```text +Generates a `VerifyingKey` from a `Circuit` instance. + +Automatically determines the smallest `k` required for the given circuit +and adjusts the received parameters to match the circuit's size. +Use `keygen_vk_with_k` to specify a custom `k` value. +``` + +### `plonk/keygen.rs:L230` (item-doc) + +```text +Generate a `VerifyingKey` from an instance of `Circuit`. +``` + +### `plonk/keygen.rs:L266` (line) + +```text +Synthesize the circuit to obtain URS +``` + +### `plonk/keygen.rs:L275-L276` (line) + +```text +After this, the ConstraintSystem should not have any selectors: `verify` does +not need them, and `keygen_pk` regenerates `cs` from scratch anyways. +``` + +### `plonk/keygen.rs:L293` (item-doc) + +```text +Generate a `ProvingKey` from a `VerifyingKey` and an instance of `Circuit`. +``` + +### `plonk/keygen.rs:L321` (line) + +```text +Synthesize the circuit to obtain URS +``` + +### `plonk/keygen.rs:L344` (line) + +```text +Compute the optimized evaluation data structure +``` + +### `plonk/keygen.rs:L367-L368` (line) + +```text +Compute l_0(X) +TODO: this can be done more efficiently +``` + +### `plonk/keygen.rs:L374-L375` (line) + +```text +Compute l_blind(X) which evaluates to 1 for each blinding factor row +and 0 otherwise over the domain. +``` + +### `plonk/keygen.rs:L383-L384` (line) + +```text +Compute l_last(X) which evaluates to 1 on the first inactive row (just +before the blinding factors) and 0 otherwise over the domain +``` + +### `plonk/keygen.rs:L391` (line) + +```text +Compute l_active_row(X) +``` + + +## `plonk/linearization/prover.rs` + +### `plonk/linearization/prover.rs:L10-L40` (item-doc) + +```text +Construct the linearization polynomial: + + `S_0(T) * id_0(x) + y * S_1(T) * id_1(x) + ... + y^m * S_m(T) * id_m(x) + - (h_0(T) + x^{n-1} * h_1(T) + ... + x^{l*(n-1)} * h_l(T)) * (x^n-1),` + +where: +* `y` is the batching challenge, +* `x` is the evaluation challenge, +* `id_j(x)` is a (partially or fully) evaluated identity at `x`, +* `S_j(T)` is, either, + - (i) the polynomial of a fixed column corresponding to a simple, + multiplicative selector, or, + - (ii) 1 (in case the corresponding identity `id_j` has been fully + evaluated and, thus, the resulting scalar `id_j(x)` contributes to + the affine term `C` of the linearization polynomial), +* `h_k(T)` are the limbs of the quotient polynomial. + +The linearization polynomial is split into its non-constant and constant +parts: `L(X) = L'(X) + C`. Both parts are returned separately. + +# Arguments + +* `expressions` - the output of + [crate::plonk::partially_evaluate_identities] +* `splitting_factor` - the evaluated splitting factor `x^{n-1}` from + decomposing the quotient polynomial `h(T)` into limbs + +# Returns + +A tuple `(L'(X), C)`, where `L'(X)` is a [Polynomial] and `C` is a constant. +The verifier uses `-C` as the expected evaluation of `L'(X)` at `x`. +``` + +### `plonk/linearization/prover.rs:L60-L61` (line) + +```text +The constant term is excluded from L'(X). It is moved to the +eval side of the VerifierQuery (as -C) by the verifier. +``` + +### `plonk/linearization/prover.rs:L71-L76` (line) + +```text +When the `single-h-commitment` feature is enabled `quotient_limbs` contains a +single element: the full quotient polynomial H(X). In that case this +loop executes once and produces `lin_poly - (x^n - 1) * H(X)`, which +evaluates to zero at `x` iff the circuit is satisfied (same as the +multi-limb case). The resulting polynomial has degree deg(H), so +the caller must supply params with a sufficiently large SRS. +``` + + +## `plonk/linearization/verifier.rs` + +### `plonk/linearization/verifier.rs:L10-L42` (item-doc) + +```text +Construct the commitment to the linearization polynomial +(which will be checked that it opens to `0` at `x` in the multi-open +argument): + + `S_0 * id_0(x) + y * S_1 * id_1(x) + ... + y^m * S_m * id_m(x) + - (h_0 + x^{n-1} * h_1 + ... + x^{l*(n-1)} * h_l) * (x^n-1),` + +where: +* `y` is the batching challenge, +* `x` is the evaluation challenge, +* `id_j(x)` is a (partially or fully) evaluated identity at `x`, +* `S_j` is, either, + - (i) the commitment to a fixed column representing a simple, + multiplicative selector, or, + - (ii) the commitment to the constant polynomial `P(X) = 1` (in case + the corresponding identity `id_j` has been fully evaluated and, thus, + the resulting scalar is part of the constant term of the + linearization polynomial) +* `h_k` are commitments to the limbs of the quotient polynomial. + +# Arguments + +* `expressions` - the output of + [crate::plonk::partially_evaluate_identities] +* `splitting_factor` - the evaluated splitting factor `x^{n-1}` from + decomposing the quotient polynomial `h(T)` into limbs + +# Returns + +A [VerifierQuery], that checks if the commitment to the linearization +polynomial opens to `0` at the evaluation challenge `x`. The commitment +itself is an MSM represented as a vector of points and a vector +of scalars. +``` + +### `plonk/linearization/verifier.rs:L72` (line) + +```text +Group multiples of the same point in the MSM +``` + +### `plonk/linearization/verifier.rs:L88-L89` (line) + +```text +Fully evaluated identities are not included and pass (negated) +to the evaluation side. +``` + + +## `plonk/logup/prover.rs` + +### `plonk/logup/prover.rs:L1-L12` (line) + +```text +This file is part of MIDNIGHT-ZK. +Copyright (C) 2025 Midnight Foundation +SPDX-License-Identifier: Apache-2.0 +Licensed under the Apache License, Version 2.0 (the "License"); +You may not use this file except in compliance with the License. +You may obtain a copy of the License at +http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` + +### `plonk/logup/prover.rs:L14-L21` (module-doc) + +```text +Prover implementation for the LogUp lookup argument. + +Constructs and commits to three polynomials: +- **Multiplicities `m(X)`**: Counts how many times each table entry is + looked up +- **Helper `h(X)`**: Aggregates at each row `Σⱼ 1/(fâ±¼(X) + β)`, where j + iterates over columns +- **Accumulator `Z(X)`**: Running sum of log-derivative differences +``` + +### `plonk/logup/prover.rs:L43` (item-doc) + +```text +Committed LogUp polynomials in coefficient form. +``` + +### `plonk/logup/prover.rs:L52-L55` (item-doc) + +```text +Computed multiplicities. + +This structure holds the multiplicity counts computed from compressing +input and table expressions. +``` + +### `plonk/logup/prover.rs:L65` (item-doc) + +```text +Committed polynomials after evaluation at the challenge point. +``` + +### `plonk/logup/prover.rs:L72-L77` (item-doc) + +```text +Compresses input and table expressions and computes the multiplicities, +committing to them. + +This method evaluates and compresses the input/table expressions using +θ-batching, then counts how many times each table entry appears in the +inputs. +``` + +### `plonk/logup/prover.rs:L115` (line) + +```text +Closure to get values of expressions and compress them +``` + +### `plonk/logup/prover.rs:L161-L165` (item-doc) + +```text +Constructs and commits to the LogUp prover polynomials. + +Compresses input expressions via θ-batching, computes the helper +polynomial using batch inversion, builds the running sum +accumulator, and commits all three to the transcript. +``` + +### `plonk/logup/prover.rs:L182-L184` (line) + +```text +We need to compute the helper polynomial, for which we need to do batch +inversion for the table. +T(X) = 1 / (t(X) + beta) +``` + +### `plonk/logup/prover.rs:L194-L196` (line) + +```text +F(X) = 1 / (f(X) + beta) +Invert each column independently in parallel, then sum across columns +to form the helper polynomial Σⱼ 1/(fâ±¼(X) + β). +``` + +### `plonk/logup/prover.rs:L229-L236` (line) + +```text +Polynomial over which we compute the running sum: + logderivative_poly[i] = selector[i]·h[i] - m[i]/(t[i]+β) + +The selector applies only to the input side (h), not to the multiplicities +(m). m[i] counts how many selected inputs reference the table value +t[i], so it lives on table rows — not input rows. Gating m by the +selector would incorrectly exclude those table contributions, +breaking the logup balance. +``` + +### `plonk/logup/prover.rs:L252` (line) + +```text +Take all rows including the "last" row. +``` + +### `plonk/logup/prover.rs:L254` (line) + +```text +Chain random blinding factors. +``` + +### `plonk/logup/prover.rs:L264` (line) + +```text +l_0(X) * z(X) = 0 +``` + +### `plonk/logup/prover.rs:L267` (line) + +```text +Running sum must be zero at last active row for LogUp to be sound +``` + +### `plonk/logup/prover.rs:L290-L291` (item-doc) + +```text +Evaluates `m(x)`, `h(x)`, `Z(x)`, and `Z(ωx)`, writing them to the +transcript. +``` + +### `plonk/logup/prover.rs:L334` (item-doc) + +```text +Returns opening queries. +``` + +### `plonk/logup/prover.rs:L368-L384` (item-doc) + +```text +Computes the multiplicity of each value in the polynomial. + +Returns a vector where `result[i]` is the number of times `table[i]` appears +in `values`. + +When a value appears multiple times in the table, the multiplicity is +normalized: if a value is looked up `k` times and appears `t` times in the +table, each table position gets multiplicity `k/t`. + +Only values in the first `usable_rows` are counted for both inputs and +table. Blinding rows are excluded from the counting but still get a +multiplicity value (zero for values not in the active region). + +# Panics + +Panics if any selected input value (where the selector is non-zero) is not +present in `table`. +``` + +### `plonk/logup/prover.rs:L397` (line) + +```text +Count how many times each value appears in the table (active rows only) +``` + +### `plonk/logup/prover.rs:L403-L404` (line) + +```text +Count how many times each value appears in inputs (only where the selector is +non-zero). +``` + +### `plonk/logup/prover.rs:L419-L420` (line) + +```text +Build vector of table counts for batch inversion (only for active table +values) +``` + +### `plonk/logup/prover.rs:L434-L435` (line) + +```text +Compute normalized multiplicities: input_count / table_count +Blinding rows get random values to ensure ZK. +``` + +### `plonk/logup/prover.rs:L470` (line) + +```text +Table with unique values: [1, 2, 3, 4] +``` + +### `plonk/logup/prover.rs:L478-L480` (line) + +```text +Two input polynomials to test aggregation across multiple inputs +input1: [1, 2, 3, 3] +input2: [2, 2, 3, 4] +``` + +### `plonk/logup/prover.rs:L494-L498` (line) + +```text +Expected counts across both inputs (all 4 rows are usable): +- 1 appears 1 time +- 2 appears 3 times (1 in input1, 2 in input2) +- 3 appears 3 times (2 in input1, 1 in input2) +- 4 appears 1 time +``` + +### `plonk/logup/prover.rs:L518` (line) + +```text +Table with values: [1, 2, 3, 4] +``` + +### `plonk/logup/prover.rs:L526` (line) + +```text +Input contains value 5, which is NOT in the table +``` + +### `plonk/logup/prover.rs:L534` (line) + +```text +Should panic because input value 5 is not found in the table +``` + +### `plonk/logup/prover.rs:L546` (line) + +```text +Table: [1, 2, 2, 3] - value 2 appears twice +``` + +### `plonk/logup/prover.rs:L554` (line) + +```text +Input looks up: 1 once, 2 twice, 3 once +``` + +### `plonk/logup/prover.rs:L572` (line) + +```text +Value 2: looked up 2 times, appears 2 times in table -> each gets 2/2 = 1 +``` + +### `plonk/logup/prover.rs:L580-L581` (line) + +```text +Table: [1, 2, 0, 0] - last 2 rows are "blinding" with default 0 +Only first 2 rows are usable +``` + +### `plonk/logup/prover.rs:L589` (line) + +```text +Input: [1, 2, random, random] - but we only count first 2 rows +``` + + +## `plonk/logup/verifier.rs` + +### `plonk/logup/verifier.rs:L1-L12` (line) + +```text +This file is part of MIDNIGHT-ZK. +Copyright (C) 2025 Midnight Foundation +SPDX-License-Identifier: Apache-2.0 +Licensed under the Apache License, Version 2.0 (the "License"); +You may not use this file except in compliance with the License. +You may obtain a copy of the License at +http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` + +### `plonk/logup/verifier.rs:L14` (module-doc) + +```text +Verifier implementation for the LogUp lookup argument. +``` + +### `plonk/logup/verifier.rs:L29` (item-doc) + +```text +Commitment to the shared multiplicity polynomial, read from the transcript. +``` + +### `plonk/logup/verifier.rs:L34-L36` (item-doc) + +```text +Commitments to all LogUp polynomials for a [`ChunkedArgument`]. + +One shared `m` and `Z`, plus one `háµ¢` per chunk. +``` + +### `plonk/logup/verifier.rs:L40` (item-doc) + +```text +One commitment per chunk of the batched argument. +``` + +### `plonk/logup/verifier.rs:L45` (item-doc) + +```text +Commitments plus evaluations at the challenge point. +``` + +### `plonk/logup/verifier.rs:L52` (item-doc) + +```text +Reads the multiplicities commitment from the transcript. +``` + +### `plonk/logup/verifier.rs:L75-L76` (item-doc) + +```text +Reads `nb_chunks` helper commitments and one accumulator commitment +from the transcript. +``` + +### `plonk/logup/verifier.rs:L114-L117` (item-doc) + +```text +Reads the polynomial evaluations from the transcript. + +Order: `m_eval`, then `háµ¢_eval` for each batched chunk, then `Z_eval`, +`Z(ωx)_eval`. +``` + +### `plonk/logup/verifier.rs:L172` (item-doc) + +```text +Returns verification queries for all committed polynomials. +``` + + +## `plonk/logup.rs` + +### `plonk/logup.rs:L1-L12` (line) + +```text +This file is part of MIDNIGHT-ZK. +Copyright (C) 2025 Midnight Foundation +SPDX-License-Identifier: Apache-2.0 +Licensed under the Apache License, Version 2.0 (the "License"); +You may not use this file except in compliance with the License. +You may obtain a copy of the License at +http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` + +### `plonk/logup.rs:L14-L133` (module-doc) + +````text +# LogUp Lookup Argument with Selector + +This module implements a selector-extended variant of the +[LogUp (Logarithmic Derivative) lookup argument](https://eprint.iacr.org/2022/1530), +adapted for univariate polynomials in the PLONK arithmetization. LogUp +provides an efficient way to prove that a set of values is contained +within a predefined table. + +The original LogUp protocol operates over multilinear polynomials and uses +the sum-check protocol. Our implementation adapts this to the univariate +setting used in PLONK, replacing sum-check with a running sum accumulator +approach. + +## The Core Idea + +Given lookup values `fâ‚, ..., fâ‚–` and a table `T = {tâ‚, ..., tâ‚™}`, the +logarithmic derivative relation Σⱼ 1/(fâ±¼ + β) = Σᵢ máµ¢/(táµ¢ + β) +characterizes table membership as follows: + +Completeness: +If fâ±¼ ∈ T for every j, then there exists `{máµ¢}áµ¢` such that +Σⱼ 1/(fâ±¼ + β) = Σᵢ máµ¢/(táµ¢ + β), for all β. +Here, `máµ¢` is the multiplicity of `táµ¢` (how many times it appears among the +`fâ±¼`s). + +Soundness: +If fⱼ∉T for some j, then for every `{máµ¢}áµ¢` it holds +Σⱼ 1/(fâ±¼ + β) ≠ Σᵢ máµ¢/(táµ¢ + β) w.o.p over a uniformly random choice of β. + +This result follows from partial fraction decomposition. + +Note: When duplicate values exist in the table, multiplicities are +normalized: if value `v` is looked up `k` times and appears `t` times in the +table, multiplicities are normalized as `k/t`. +(The table may contain duplicates because it is padded to the domain size.) + +## Selector Extension + +Each lookup argument carries an optional **selector** `s(X)` that restricts +which rows participate in the lookup check. When `s(X) = 0` at a row, that +row's input values are ignored; when `s(X) = 1`, the row is active and its +inputs must be in the table. + +The selector modifies the balance equation to: +```text +Σᵢ s(Xáµ¢)·h(Xáµ¢) = Σᵢ m(Xáµ¢)/(t(Xáµ¢) + β) +``` + +Critically, **the selector is only applied to the input side** (`h`). +Multiplicities `m` are always summed over all table rows, because they count +how many *selected* input rows reference each table entry, so +`Σᵢ máµ¢/(táµ¢ + β)` evaluates to `Σᵢ sᵢ·háµ¢` unconditionally. +Applying the selector to `m` too would silently drop table-row contributions +and break the balance. + +## Running Sum Formulation + +Rather than checking the sum equality directly (which would require +sum-check in the multilinear setting), we encode the constraint as a running +sum over the evaluation domain. We introduce: + +- **Helper polynomial** `h(X)`: Encodes `Σⱼ 1/(fâ±¼(X) + β)` at each row +- **Multiplicities** `m(X)`: Counts how many times each table entry is used + by selected input rows +- **Accumulator** `Z(X)`: Running sum that accumulates the log-derivative + differences, for every i: Záµ¢ = Σ_{j, F)` containing an evaluation point (representing a +partially or fully evaluated identity at `x`) and an [Option] which +references: + * the fixed column index of a simple, multiplicative selector, if this + evaluation point is multiplied by such a selector, + * `None` otherwise. +``` + +### `plonk/mod.rs:L524` (line) + +```text +Evaluate the circuit using the custom gates provided +``` + + +## `plonk/permutation/keygen.rs` + +### `plonk/permutation/keygen.rs:L16-L17` (item-doc) + +```text +Struct that accumulates all the necessary data in order to construct the +permutation argument. +``` + +### `plonk/permutation/keygen.rs:L20` (item-doc) + +```text +Columns that participate on the copy permutation argument. +``` + +### `plonk/permutation/keygen.rs:L22` (item-doc) + +```text +Mapping of the actual copies done. +``` + +### `plonk/permutation/keygen.rs:L24` (item-doc) + +```text +Some aux data used to swap positions directly when sorting. +``` + +### `plonk/permutation/keygen.rs:L26` (item-doc) + +```text +More aux data +``` + +### `plonk/permutation/keygen.rs:L32-L33` (line) + +```text +Initialize the copy vector to keep track of copy constraints in all +the permutation arguments. +``` + +### `plonk/permutation/keygen.rs:L36` (line) + +```text +Computes [(i, 0), (i, 1), ..., (i, n - 1)] +``` + +### `plonk/permutation/keygen.rs:L40-L42` (line) + +```text +Before any equality constraints are applied, every cell in the permutation is +in a 1-cycle; therefore mapping and aux are identical, because every cell is +its own distinguished element. +``` + +### `plonk/permutation/keygen.rs:L69` (line) + +```text +Check bounds +``` + +### `plonk/permutation/keygen.rs:L76` (line) + +```text +See book/src/design/permutation.md for a description of this algorithm. +``` + +### `plonk/permutation/keygen.rs:L81` (line) + +```text +If left and right are in the same cycle, do nothing. +``` + +### `plonk/permutation/keygen.rs:L90` (line) + +```text +Merge the right cycle into the left one. +``` + +### `plonk/permutation/keygen.rs:L125` (item-doc) + +```text +Returns columns that participate in the permutation argument. +``` + +### `plonk/permutation/keygen.rs:L130` (item-doc) + +```text +Returns mappings of the copies. +``` + +### `plonk/permutation/keygen.rs:L143` (line) + +```text +Compute [omega^0, omega^1, ..., omega^{params.n - 1}] +``` + +### `plonk/permutation/keygen.rs:L156-L157` (line) + +```text +Compute [omega_powers * \delta^0, omega_powers * \delta^1, ..., omega_powers +* \delta^m] +``` + +### `plonk/permutation/keygen.rs:L171` (line) + +```text +Compute permutation polynomials, convert to coset form. +``` + +### `plonk/permutation/keygen.rs:L200` (line) + +```text +Compute [omega^0, omega^1, ..., omega^{params.n - 1}] +``` + +### `plonk/permutation/keygen.rs:L213-L214` (line) + +```text +Compute [omega_powers * \delta^0, omega_powers * \delta^1, ..., omega_powers +* \delta^m] +``` + +### `plonk/permutation/keygen.rs:L228-L229` (line) + +```text +Computes the permutation polynomial based on the permutation +description in the assembly. +``` + +### `plonk/permutation/keygen.rs:L243` (line) + +```text +Pre-compute commitments for the URS. +``` + +### `plonk/permutation/keygen.rs:L246` (line) + +```text +Compute commitment to permutation polynomial +``` + + +## `plonk/permutation/prover.rs` + +### `plonk/permutation/prover.rs:L64-L67` (line) + +```text +How many columns can be included in a single permutation polynomial? +We need to multiply by z(X) and (1 - (l_last(X) + l_blind(X))). This +will never underflow because of the requirement of at least a degree +3 circuit for the permutation argument. +``` + +### `plonk/permutation/prover.rs:L72` (line) + +```text +Each column gets its own delta power. +``` + +### `plonk/permutation/prover.rs:L75` (line) + +```text +Track the "last" value from the previous column set +``` + +### `plonk/permutation/prover.rs:L83-L89` (line) + +```text +Goal is to compute the products of fractions + +(p_j(\omega^i) + \delta^j \omega^i \beta + \gamma) / +(p_j(\omega^i) + \beta s_j(\omega^i) + \gamma) + +where p_j(X) is the jth column in this permutation, +and i is the ith row of the column. +``` + +### `plonk/permutation/prover.rs:L93` (line) + +```text +Iterate over each column of the permutation +``` + +### `plonk/permutation/prover.rs:L111` (line) + +```text +Invert to obtain the denominator for the permutation product polynomial +``` + +### `plonk/permutation/prover.rs:L114-L115` (line) + +```text +Iterate over each column again, this time finishing the computation +of the entire fraction by computing the numerators +``` + +### `plonk/permutation/prover.rs:L128` (line) + +```text +Multiply by p_j(\omega^i) + \delta^j \omega^i \beta +``` + +### `plonk/permutation/prover.rs:L136-L143` (line) + +```text +The modified_values vector is a vector of products of fractions +of the form + +(p_j(\omega^i) + \delta^j \omega^i \beta + \gamma) / +(p_j(\omega^i) + \beta s_j(\omega^i) + \gamma) + +where i is the index into modified_values, for the jth column in +the permutation +``` + +### `plonk/permutation/prover.rs:L145-L146` (line) + +```text +Compute the evaluations of the permutation product polynomial +over our domain, starting with z[0] = 1 +``` + +### `plonk/permutation/prover.rs:L154` (line) + +```text +Set blinding factors +``` + +### `plonk/permutation/prover.rs:L158` (line) + +```text +Set new last_z +``` + +### `plonk/permutation/prover.rs:L164` (line) + +```text +Hash the permutation product commitment +``` + +### `plonk/permutation/prover.rs:L190` (line) + +```text +Hash permutation evals +``` + +### `plonk/permutation/prover.rs:L225` (line) + +```text +Hash permutation product evals +``` + +### `plonk/permutation/prover.rs:L233-L235` (line) + +```text +If we have any remaining sets to process, evaluate this set at omega^u +so we can constrain the last value of its running product to equal the +first value of the next set's running product, chaining them together. +``` + +### `plonk/permutation/prover.rs:L274` (line) + +```text +Open permutation product commitments at x and \omega x +``` + +### `plonk/permutation/prover.rs:L284-L286` (line) + +```text +Open it at \omega^{last} x for all but the last set. This rotation is only +sensical for the first row, but we only use this rotation in a constraint +that is gated on l_0. +``` + + +## `plonk/permutation/verifier.rs` + +### `plonk/permutation/verifier.rs:L157` (line) + +```text +Open permutation product commitments at x and \omega x +``` + +### `plonk/permutation/verifier.rs:L171` (line) + +```text +Open it at \omega^{last} x for all but the last set +``` + +### `plonk/permutation/verifier.rs:L191` (line) + +```text +Open permutation commitments for each permutation argument at x +``` + + +## `plonk/permutation.rs` + +### `plonk/permutation.rs:L1` (module-doc) + +```text +Implementation of permutation argument. +``` + +### `plonk/permutation.rs:L31` (item-doc) + +```text +A permutation argument. +``` + +### `plonk/permutation.rs:L34` (item-doc) + +```text +A sequence of columns involved in the argument. +``` + +### `plonk/permutation.rs:L43-L45` (item-doc) + +```text +Returns the minimum circuit degree required by the permutation argument. +The argument may use larger degree gates depending on the actual +circuit's degree and how many columns are involved in the permutation. +``` + +### `plonk/permutation.rs:L47-L74` (line) + +```text +degree 2: +l_0(X) * (1 - z(X)) = 0 + +We will fit as many polynomials p_i(X) as possible +into the required degree of the circuit, so the +following will not affect the required degree of +this middleware. + +(1 - (l_last(X) + l_blind(X))) * ( + z(\omega X) \prod (p(X) + \beta s_i(X) + \gamma) +- z(X) \prod (p(X) + \delta^i \beta X + \gamma) +) + +On the first sets of columns, except the first +set, we will do + +l_0(X) * (z(X) - z'(\omega^(last) X)) = 0 + +where z'(X) is the permutation for the previous set +of columns. + +On the final set of columns, we will do + +degree 3: +l_last(X) * (z'(X)^2 - z'(X)) = 0 + +which will allow the last value to be zero to +ensure the argument is perfectly complete. +``` + +### `plonk/permutation.rs:L76-L77` (line) + +```text +There are constraints of degree 3 regardless of the +number of columns involved. +``` + +### `plonk/permutation.rs:L87` (item-doc) + +```text +Returns columns that participate on the permutation argument. +``` + +### `plonk/permutation.rs:L93` (item-doc) + +```text +The verifying key for a single permutation argument. +``` + +### `plonk/permutation.rs:L100` (item-doc) + +```text +Returns the (permutation argument) commitments of the verifying key. +``` + +### `plonk/permutation.rs:L137` (item-doc) + +```text +The proving key for a single permutation argument. +``` + +### `plonk/permutation.rs:L146-L147` (item-doc) + +```text +Reads proving key for a single permutation argument from buffer using +`Polynomial::read`. +``` + +### `plonk/permutation.rs:L163-L164` (item-doc) + +```text +Writes proving key for a single permutation argument to buffer using +`Polynomial::write`. +``` + +### `plonk/permutation.rs:L172` (item-doc) + +```text +Gets the total number of bytes in the serialization of `self` +``` + +### `plonk/permutation.rs:L204-L205` (line) + +```text +Enforce only for the first set. +l_0(X) * (1 - z_0(X)) = 0 +``` + +### `plonk/permutation.rs:L210-L211` (line) + +```text +Enforce only for the last set. +l_last(X) * (z_l(X)^2 - z_l(X)) = 0 +``` + +### `plonk/permutation.rs:L216-L217` (line) + +```text +Except for the first set, enforce. +l_0(X) * (z_i(X) - z_{i-1}(\omega^(last) X)) = 0 +``` + +### `plonk/permutation.rs:L230-L234` (line) + +```text +And for all the sets we enforce: +(1 - (l_last(X) + l_blind(X))) * ( + z_i(\omega X) \prod (p(X) + \beta s_i(X) + \gamma) +- z_i(X) \prod (p(X) + \delta^i \beta X + \gamma) +) +``` + + +## `plonk/prover.rs` + +### `plonk/prover.rs:L36-L51` (item-doc) + +```text +Computes the quotient polynomial `h(X) = nu(X) / (X^n - 1)` and commits to +it, writing the commitment(s) to the transcript. + +**Default behaviour** (`single-h-commitment` feature *disabled*): `h(X)` is +split into `quotient_poly_degree` limbs of degree `n-2` each, so that each +can be committed with an SRS of size `n` after 1 term for blinding. Each +limb is independently blinded and committed, and all `quotient_poly_degree` +commitments are written to the transcript. The returned `Vec` contains the +`quotient_poly_degree` limb polynomials in coefficient form. + +**Alternative behaviour** (`single-h-commitment` feature *enabled*): `h(X)` +is committed as a single polynomial without splitting. A single commitment +is written to the transcript. The returned `Vec` contains exactly one +element: the full `h(X)` in coefficient form. In this mode the `params` +**must** supply an SRS of at least `(n-1) * quotient_poly_degree` elements +(i.e., params generated with `k' >= log2(n * (d-1))`). +``` + +### `plonk/prover.rs:L65` (line) + +```text +Construct quotient polynomial h(X) = nu(X) / (X^n - 1) in evaluation form +``` + +### `plonk/prover.rs:L68` (line) + +```text +Convert h(X) to coefficient form +``` + +### `plonk/prover.rs:L71-L74` (line) + +```text +Let n := size of evaluation domain +Let d := degree of constraint system +Hence, the degree of the quotient poly is: d*(n-1) - n = (d-1)*(n-1) - 1, +and a domain of size (d-1)*(n-1) suffices to correctly represent it +``` + +### `plonk/prover.rs:L77-L78` (line) + +```text +When the single-h-commitment feature is enabled, commit to h(X) in one go. +The params SRS must have at least h_poly.len() monomial elements. +``` + +### `plonk/prover.rs:L94-L95` (line) + +```text +Split h(X) up into limbs and add inter-limb blinding so that +individual limb commitments do not leak information about h(X). +``` + +### `plonk/prover.rs:L108` (line) + +```text +Compute commitment to each limb +``` + +### `plonk/prover.rs:L111` (line) + +```text +Write each limb commitment to the transcript +``` + +### `plonk/prover.rs:L135-L137` (item-doc) + +```text +Commit to a vector of raw instances. This function can be used to prepare +the committed instances that the verifier will be provided with when this +feature is enabled. +``` + +### `plonk/prover.rs:L153-L158` (item-doc) + +```text +This computes a proof trace for the provided `circuits` when given the +public parameters `params` and the proving key [`ProvingKey`] that was +generated previously for the same circuit. The provided `instances` +are zero-padded internally. + +The trace can then be used to finalise proofs, or to fold them. +``` + +### `plonk/prover.rs:L168-L170` (line) + +```text +The prover needs to get all instances in non-committed form. However, +the first `nb_committed_instances` instance columns are dedicated for +instances that the verifier receives in committed form. +``` + +### `plonk/prover.rs:L200` (line) + +```text +Hash verification key into transcript +``` + +### `plonk/prover.rs:L210` (line) + +```text +Sample theta challenge for keeping lookup columns linearly independent +``` + +### `plonk/prover.rs:L213` (line) + +```text +Commit to the multiplicities columns +``` + +### `plonk/prover.rs:L239` (line) + +```text +Sample beta challenge +``` + +### `plonk/prover.rs:L242` (line) + +```text +Sample gamma challenge +``` + +### `plonk/prover.rs:L245` (line) + +```text +Commit to permutations. +``` + +### `plonk/prover.rs:L268` (line) + +```text +Construct and commit to products polynomials for each lookup +``` + +### `plonk/prover.rs:L276` (line) + +```text +Trash argument +``` + +### `plonk/prover.rs:L303` (line) + +```text +Obtain challenge for keeping all separate gates linearly independent +``` + +### `plonk/prover.rs:L335-L339` (item-doc) + +```text +This takes the computed trace of a set of witnesses and creates a proof +for the provided `circuit` when given the public +parameters `params` and the proving key [`ProvingKey`] that was +generated previously for the same circuit. The provided `instances` +are zero-padded internally. +``` + +### `plonk/prover.rs:L343-L345` (line) + +```text +The prover needs to get all instances in non-committed form. However, +the first `nb_committed_instances` instance columns are dedicated for +instances that the verifier receives in committed form. +``` + +### `plonk/prover.rs:L364-L365` (line) + +```text +Construct the quotient polynomial h(X) = nu(X)/(X^n-1), split it into limbs, +and commit to each limb separately +``` + +### `plonk/prover.rs:L400` (line) + +```text +Evaluate common permutation data +``` + +### `plonk/prover.rs:L403` (line) + +```text +Evaluate the permutations, if any, at omega^i x. +``` + +### `plonk/prover.rs:L409` (line) + +```text +Evaluate the lookups, if any, at omega^i x. +``` + +### `plonk/prover.rs:L420` (line) + +```text +Evaluate the trashcans, if any, at x. +``` + +### `plonk/prover.rs:L431-L432` (line) + +```text +Partially evaluate batched identities (without fixed columns +corresponding to simple, multiplicative selectors) +``` + +### `plonk/prover.rs:L453` (line) + +```text +Compute linearization polynomial +``` + +### `plonk/prover.rs:L478-L481` (item-doc) + +```text +This creates a proof for the provided `circuit` when given the public +parameters `params` and the proving key [`ProvingKey`] that was +generated previously for the same circuit. The provided `instances` +are zero-padded internally. +``` + +### `plonk/prover.rs:L482-L484` (line) + +```text + +NOTE: Any change here must be mirrored in src/plonk/bench/prover.rs +to ensure the benchmarks remain aligned with the real prover. +``` + +### `plonk/prover.rs:L553` (line) + +```text +Committed instances go first. +``` + +### `plonk/prover.rs:L624-L625` (line) + +```text +Selector optimizations cannot be applied here; use the ConstraintSystem +from the verification key. +``` + +### `plonk/prover.rs:L660-L663` (line) + +```text +The prover will not be allowed to assign values to advice +cells that exist within inactive rows, which include some +number of blinding factors and an extra row for use in the +permutation argument. +``` + +### `plonk/prover.rs:L668` (line) + +```text +Synthesize the circuit to obtain the witness and other information. +``` + +### `plonk/prover.rs:L749` (line) + +```text +Calculate the advice and instance cosets +``` + +### `plonk/prover.rs:L769-L771` (line) + +```text +Evaluate the numerator polynomial nu(X) of the quotient polynomial +h(X) = nu(X) / (X^n-1): nu(X) is a random linear combination of all +independent identities +``` + +### `plonk/prover.rs:L794` (line) + +```text +Structure for holding evaluations of fixed, instance, and advice columns. +``` + +### `plonk/prover.rs:L821-L822` (line) + +```text +Compute and hash evals for the polynomials of the committed instances of +each circuit +``` + +### `plonk/prover.rs:L826` (line) + +```text +Evaluate polynomials at omega^i x +``` + +### `plonk/prover.rs:L841` (line) + +```text +Compute and hash advice evals for each circuit instance +``` + +### `plonk/prover.rs:L845` (line) + +```text +Evaluate polynomials at omega^i x +``` + +### `plonk/prover.rs:L856-L860` (line) + +```text +Compute evals of fixed columns (shared across all circuit instances), +and write them to the transcript + +NB: Fixed columns corresponding to simple, multiplicative selectors don't +need to be evaluated, nor written to the transcript +``` + +### `plonk/prover.rs:L936` (line) + +```text +Filter out queries for simple, multiplicative selectors +``` + +### `plonk/prover.rs:L979` (line) + +```text +Do nothing; we don't care about regions in this context. +``` + +### `plonk/prover.rs:L983` (line) + +```text +Do nothing; we don't care about regions in this context. +``` + +### `plonk/prover.rs:L991` (line) + +```text +We only care about advice columns here +``` + +### `plonk/prover.rs:L1001` (line) + +```text +Do nothing +``` + +### `plonk/prover.rs:L1029` (line) + +```text +Ignore assignment of advice column in different phase than current one. +``` + +### `plonk/prover.rs:L1060` (line) + +```text +We only care about advice columns here +``` + +### `plonk/prover.rs:L1066` (line) + +```text +We only care about advice columns here +``` + +### `plonk/prover.rs:L1093` (line) + +```text +Do nothing; we don't care about namespaces in this context. +``` + +### `plonk/prover.rs:L1097` (line) + +```text +Do nothing; we don't care about namespaces in this context. +``` + +### `plonk/prover.rs:L1144` (line) + +```text +Create proof with wrong number of instances +``` + +### `plonk/prover.rs:L1157` (line) + +```text +Create proof with correct number of instances +``` + + +## `plonk/solidity_trace.rs` + +### `plonk/solidity_trace.rs:L1-L6` (module-doc) + +```text +Trace hooks for differential testing of generated Solidity verifiers. + +This module is intentionally small and byte-oriented: the Rust verifier +records the exact scalar/G1 bytes that correspond to generated Solidity +trace logs, and downstream harnesses can compare those bytes without +reimplementing verifier algebra. +``` + +### `plonk/solidity_trace.rs:L12` (item-doc) + +```text +First trace topic used for proof G1 commitments, in proof-read order. +``` + +### `plonk/solidity_trace.rs:L14` (item-doc) + +```text +First trace topic used for proof scalar evaluations, in proof-read order. +``` + +### `plonk/solidity_trace.rs:L16` (item-doc) + +```text +First trace topic used for user-phase transcript challenges. +``` + +### `plonk/solidity_trace.rs:L18` (item-doc) + +```text +First trace topic used for raw quotient identity evaluations. +``` + +### `plonk/solidity_trace.rs:L20` (item-doc) + +```text +Trace topic used for the fully grouped quotient numerator `nu_y(x)`. +``` + +### `plonk/solidity_trace.rs:L22` (item-doc) + +```text +First trace topic used for per-point-set PCS `q_com` commitments. +``` + +### `plonk/solidity_trace.rs:L24` (item-doc) + +```text +First trace topic used for serialized PCS point sets. +``` + +### `plonk/solidity_trace.rs:L26` (item-doc) + +```text +Trace topic used for the materialized linearization commitment. +``` + +### `plonk/solidity_trace.rs:L28` (item-doc) + +```text +Trace topic used for final selector-fold scalars. +``` + +### `plonk/solidity_trace.rs:L30` (item-doc) + +```text +Trace topic used for the final pairing result. +``` + +### `plonk/solidity_trace.rs:L44` (item-doc) + +```text +One Rust verifier trace item, keyed by the generated Solidity trace ID. +``` + +### `plonk/solidity_trace.rs:L47` (item-doc) + +```text +Solidity trace topic ID. +``` + +### `plonk/solidity_trace.rs:L49` (item-doc) + +```text +Stable human-readable name for diagnostics. +``` + +### `plonk/solidity_trace.rs:L51-L52` (item-doc) + +```text +Trace payload bytes. Scalars are 32 bytes; G1 points are the +transcript's EIP-2537 padded 128-byte representation. +``` + +### `plonk/solidity_trace.rs:L56` (item-doc) + +```text +Start collecting trace events on the current thread. +``` + +### `plonk/solidity_trace.rs:L67` (item-doc) + +```text +Stop collecting and return all events collected on the current thread. +``` + +### `plonk/solidity_trace.rs:L72` (item-doc) + +```text +Record an already-rendered byte payload under a Solidity trace topic ID. +``` + +### `plonk/solidity_trace.rs:L85` (item-doc) + +```text +Record a `u64` as a 32-byte big-endian Solidity word. +``` + +### `plonk/solidity_trace.rs:L92` (item-doc) + +```text +Record a transcript-hashable value using its diagnostic trace bytes. +``` + +### `plonk/solidity_trace.rs:L102` (item-doc) + +```text +Record the next proof commitment read from the transcript. +``` + +### `plonk/solidity_trace.rs:L122` (item-doc) + +```text +Record the next proof scalar evaluation read from the transcript. +``` + + +## `plonk/traces.rs` + +### `plonk/traces.rs:L1-L2` (module-doc) + +```text +Representation of a Trace for a batch of proofs that are being generated +simultaneously. +``` + +### `plonk/traces.rs:L11-L12` (item-doc) + +```text +Prover's trace of a set of proofs. This type guarantees that the size of the +outer vector of its fields has the same size. +``` + +### `plonk/traces.rs:L18` (line) + +```text +This field will be useful for split accumulation +``` + +### `plonk/traces.rs:L31-L32` (item-doc) + +```text +Verifier's trace of a set of proofs. This type guarantees that the size of +the outer vector of its fields has the same size. +``` + + +## `plonk/trash/prover.rs` + +### `plonk/trash/prover.rs:L65` (line) + +```text +Hash permuted input commitment +``` + + +## `plonk/trash.rs` + +### `plonk/trash.rs:L18` (item-doc) + +```text +Constructs a new trash argument. +``` + +### `plonk/trash.rs:L36` (item-doc) + +```text +The name of this argument. +``` + +### `plonk/trash.rs:L41` (item-doc) + +```text +The selector of this trash argument. +``` + +### `plonk/trash.rs:L46` (item-doc) + +```text +The constraints of this trash argument. +``` + + +## `plonk/verifier.rs` + +### `plonk/verifier.rs:L21-L23` (item-doc) + +```text +Given a plonk proof, this function parses it to extract the verifying trace. +This function computes all Fiat-Shamir challenges, with the exception of +`x`, which is computed in [verify_algebraic_constraints] +``` + +### `plonk/verifier.rs:L26-L30` (line) + +```text +Unlike the prover, the verifier gets their instances in two arguments: +committed and normal (non-committed). Note that the total number of +instance columns is expected to be the sum of committed instances and +normal instances for every proof. (Committed instances go first, that is, +the first instance columns are devoted to committed instances.) +``` + +### `plonk/verifier.rs:L60` (line) + +```text +Check that instances matches the expected number of instance columns +``` + +### `plonk/verifier.rs:L69` (line) + +```text +Hash verification key into transcript +``` + +### `plonk/verifier.rs:L105-L106` (line) + +```text +Hash the prover's advice commitments into the transcript and squeeze +challenges +``` + +### `plonk/verifier.rs:L149` (line) + +```text +Sample theta challenge for keeping lookup columns linearly independent +``` + +### `plonk/verifier.rs:L154` (line) + +```text +Read multiplicities +``` + +### `plonk/verifier.rs:L157` (line) + +```text +Hash each lookup permuted commitment +``` + +### `plonk/verifier.rs:L166` (line) + +```text +Sample beta challenge +``` + +### `plonk/verifier.rs:L171` (line) + +```text +Sample gamma challenge +``` + +### `plonk/verifier.rs:L178` (line) + +```text +Hash each permutation product commitment +``` + +### `plonk/verifier.rs:L214` (line) + +```text +Sample y challenge, which keeps the gates linearly independent. +``` + +### `plonk/verifier.rs:L233-L237` (item-doc) + +```text +Given a VerifierTrace, this function computes the opening challenge, x, +and proceeds to verify the algebraic constraints with the claimed +evaluations. This function does not verify the PCS proof. + +The verifier will error if there are trailing bits in the transcript. +``` + +### `plonk/verifier.rs:L241-L245` (line) + +```text +Unlike the prover, the verifier gets their instances in two arguments: +committed and normal (non-committed). Note that the total number of +instance columns is expected to be the sum of committed instances and +normal instances for every proof. (Committed instances go first, that is, +the first instance columns are devoted to committed instances.) +``` + +### `plonk/verifier.rs:L283-L286` (line) + +```text +Read commitment(s) to the quotient polynomial h(X) = nu(X)/(X^n-1) from +the transcript. When the `single-h-commitment` feature is enabled the prover +commits to h(X) as a single polynomial (one commitment); otherwise it +splits h(X) into `quotient_poly_degree` limbs (one commitment each). +``` + +### `plonk/verifier.rs:L302-L303` (line) + +```text +Sample x challenge, which is used to ensure the circuit is +satisfied with high probability. +``` + +### `plonk/verifier.rs:L395-L397` (line) + +```text +Read (num_fixed_columns - num_simple_selectors) evals and from the transcript +and fill up the "missing" places with 1 (the transcript doesn't contain evals +corresponding to multiplicative, simple selectors) +``` + +### `plonk/verifier.rs:L475-L476` (line) + +```text +Partially evaluate batched identities +(without fixed columns corresponding to simple, multiplicative selectors) +``` + +### `plonk/verifier.rs:L576-L579` (line) + +```text +Collect queries that are checked in the multi-open argument + +NB: Queries corresponding to simple, multiplicative selectors need not be +checked +``` + +### `plonk/verifier.rs:L634` (line) + +```text +Filter out queries for simple, multiplicative selectors +``` + +### `plonk/verifier.rs:L649-L650` (line) + +```text +We are now convinced the circuit is satisfied so long as the +polynomial commitments open to the correct values. +``` + +### `plonk/verifier.rs:L654-L658` (item-doc) + +```text +Prepares a plonk proof into a PCS instance that can be finalized or +batched. It is responsibility of the verifier to check the validity of the +instance columns. + +The verifier will error if there are trailing bytes in the transcript. +``` + +### `plonk/verifier.rs:L661-L665` (line) + +```text +Unlike the prover, the verifier gets their instances in two arguments: +committed and normal (non-committed). Note that the total number of +instance columns is expected to be the sum of committed instances and +normal instances for every proof. (Committed instances go first, that is, +the first instance columns are devoted to committed instances.) +``` + + +## `poly/commitment.rs` + +### `poly/commitment.rs:L1` (module-doc) + +```text +Trait for a commitment scheme +``` + +### `poly/commitment.rs:L14-L15` (item-doc) + +```text +Public interface for a additively homomorphic Polynomial Commitment Scheme +(PCS) +``` + +### `poly/commitment.rs:L17` (item-doc) + +```text +Parameters needed to generate a proof in the PCS +``` + +### `poly/commitment.rs:L20` (item-doc) + +```text +Parameters needed to verify a proof in the PCS +``` + +### `poly/commitment.rs:L23` (item-doc) + +```text +Type of a committed polynomial +``` + +### `poly/commitment.rs:L34` (item-doc) + +```text +Verification guard. Allows for batch verification +``` + +### `poly/commitment.rs:L37` (item-doc) + +```text +Generates the parameters of the polynomial commitment scheme +``` + +### `poly/commitment.rs:L40` (item-doc) + +```text +Extract the `VerifierParameters` from `Parameters` +``` + +### `poly/commitment.rs:L43` (item-doc) + +```text +Commit to a polynomial in coefficient form +``` + +### `poly/commitment.rs:L46-L47` (item-doc) + +```text +Commit to a polynomial expressed in Lagrange evaluations form (over the +underlying domain specified in params). +``` + +### `poly/commitment.rs:L53` (item-doc) + +```text +Create a multi-opening proof at a set of [ProverQuery]'s. +``` + +### `poly/commitment.rs:L63-L64` (item-doc) + +```text +Verify an multi-opening proof for a given set of [VerifierQuery]'s. +The function fails if the transcript has trailing bytes. +``` + +### `poly/commitment.rs:L76` (item-doc) + +```text +Interface for verifier finalizer +``` + +### `poly/commitment.rs:L78` (item-doc) + +```text +Finalize the verification guard +``` + +### `poly/commitment.rs:L81` (item-doc) + +```text +Finalize a batch of verification guards +``` + +### `poly/commitment.rs:L96` (item-doc) + +```text +Interface for PCS params +``` + +### `poly/commitment.rs:L98-L100` (item-doc) + +```text +Returns the size of the Lagrange basis, expressed as the exponent `k` +such that the Lagrange domain has `2^k` elements. This equals the +circuit domain size and is used by keygen to validate the SRS. +``` + +### `poly/commitment.rs:L103-L108` (item-doc) + +```text +Returns the number of monomial-basis elements `[s^i]Gâ‚` available in +the SRS. For a standard SRS this equals `1 << max_k()`. When the +`single-h-commitment` feature is enabled the monomial basis may be +larger than the Lagrange basis (which covers only the circuit +domain), so this method returns the true capacity for +coefficient-form commitments. +``` + +### `poly/commitment.rs:L113` (item-doc) + +```text +Downsize the params to work with a circuit of size `new_k` +``` + +### `poly/commitment.rs:L116-L118` (item-doc) + +```text +Downsize the params to work with a circuit of unknown length. The +function first computes the `k` of the provided circuit, and then +downsizes the SRS. +``` + + +## `poly/domain.rs` + +### `poly/domain.rs:L1-L2` (module-doc) + +```text +Contains utilities for performing polynomial arithmetic over an evaluation +domain that is of a suitable size for the application. +``` + +### `poly/domain.rs:L13-L15` (item-doc) + +```text +This structure contains precomputed constants and other details needed for +performing operations on an evaluation domain of size $2^k$ and an extended +domain of size $2^{k} * j$ with $j \neq 0$. +``` + +### `poly/domain.rs:L35-L36` (item-doc) + +```text +This constructs a new evaluation domain object based on the provided +values $j, k$. +``` + +### `poly/domain.rs:L38` (line) + +```text +quotient_poly_degree * params.n - 1 is the degree of the quotient polynomial +``` + +### `poly/domain.rs:L41` (line) + +```text +n = 2^k +``` + +### `poly/domain.rs:L44-L46` (line) + +```text +We need to work within an extended domain, not params.k but params.k + i +for some integer i such that 2^(params.k + i) is sufficiently large to +describe the quotient polynomial. +``` + +### `poly/domain.rs:L52` (line) + +```text +ensure extended_k <= S +``` + +### `poly/domain.rs:L57-L59` (line) + +```text +Get extended_omega, the 2^{extended_k}'th root of unity +The loop computes extended_omega = omega^{2 ^ (S - extended_k)} +Notice that extended_omega ^ {2 ^ extended_k} = omega ^ {2^S} = 1. +``` + +### `poly/domain.rs:L66-L70` (line) + +```text +Get omega, the 2^{k}'th root of unity (i.e. n'th root of unity) +The loop computes omega = extended_omega ^ {2 ^ (extended_k - k)} + = (omega^{2 ^ (S - extended_k)}) ^ {2 ^ (extended_k - k)} + = omega ^ {2 ^ (S - k)}. +Notice that omega ^ {2^k} = omega ^ {2^S} = 1. +``` + +### `poly/domain.rs:L78-L80` (line) + +```text +The coset evaluation domain is: +zeta {1, extended_omega, extended_omega^2, ..., +extended_omega^{(2^extended_k) - 1}} +``` + +### `poly/domain.rs:L86-L87` (line) + +```text +Compute the evaluations of t(X) = X^n - 1 in the coset evaluation domain. +We don't have to compute all of them, because it will repeat. +``` + +### `poly/domain.rs:L100` (line) + +```text +Subtract 1 from each to give us t_evaluations[i] = t(zeta * extended_omega^i) +``` + +### `poly/domain.rs:L105-L106` (line) + +```text +Invert, because we're dividing by this polynomial. +We invert in a batch, below. +``` + +### `poly/domain.rs:L112-L113` (line) + +```text +The barycentric weight of 1 over the evaluation domain +1 / \prod_{i != 0} (1 - omega^i) +``` + +### `poly/domain.rs:L116` (line) + +```text +Compute batch inversion +``` + +### `poly/domain.rs:L144-L146` (item-doc) + +```text +Obtains a polynomial in Lagrange form when given a vector of Lagrange +coefficients of size `n`; panics if the provided vector is the wrong +length. +``` + +### `poly/domain.rs:L156-L158` (item-doc) + +```text +Obtains a polynomial in coefficient form when given a vector of +coefficients of size `n`; panics if the provided vector is the wrong +length. +``` + +### `poly/domain.rs:L168` (item-doc) + +```text +Returns an empty (zero) polynomial in the coefficient representation +``` + +### `poly/domain.rs:L176-L177` (item-doc) + +```text +Returns an empty (zero) polynomial in the Lagrange coefficient +representation +``` + +### `poly/domain.rs:L185-L186` (item-doc) + +```text +Returns an empty (zero) polynomial in the Lagrange coefficient +representation, with deferred inversions. +``` + +### `poly/domain.rs:L194` (item-doc) + +```text +Returns a constant polynomial in the Lagrange coefficient representation +``` + +### `poly/domain.rs:L202-L203` (item-doc) + +```text +Returns an empty (zero) polynomial in the extended Lagrange coefficient +representation +``` + +### `poly/domain.rs:L211-L212` (item-doc) + +```text +Returns a constant polynomial in the extended Lagrange coefficient +representation +``` + +### `poly/domain.rs:L220-L223` (item-doc) + +```text +This takes us from an n-length vector into the coefficient form. + +This function will panic if the provided vector does not have the +correct length. +``` + +### `poly/domain.rs:L227` (line) + +```text +Perform inverse FFT to obtain the polynomial in coefficient form +``` + +### `poly/domain.rs:L236-L237` (item-doc) + +```text +This takes us from an n-length coefficient vector into the +lagrange evaluation domain. +``` + +### `poly/domain.rs:L249-L250` (item-doc) + +```text +This takes us from an n-length coefficient vector into a coset of the +extended evaluation domain, rotating by `rotation` if desired. +``` + +### `poly/domain.rs:L267-L271` (item-doc) + +```text +This takes us from the extended evaluation domain and gets us the +quotient polynomial coefficients. + +This function will panic if the provided vector does not have the +correct length. +``` + +### `poly/domain.rs:L275` (line) + +```text +Inverse FFT +``` + +### `poly/domain.rs:L283-L284` (line) + +```text +Distribute powers to move from coset; opposite from the +transformation we performed earlier. +``` + +### `poly/domain.rs:L290-L294` (item-doc) + +```text +This takes us from the extended evaluation domain and gets us to the +small evaluation domain (of size `n`). + +This function will panic if the provided vector does not have the +correct length. +``` + +### `poly/domain.rs:L319-L320` (item-doc) + +```text +This divides the polynomial (in the extended domain) by the vanishing +polynomial of the $2^k$ size domain. +``` + +### `poly/domain.rs:L327-L328` (line) + +```text +Divide to obtain the quotient polynomial in the coset evaluation +domain. +``` + +### `poly/domain.rs:L342-L348` (item-doc) + +```text +Given a slice of group elements `[a_0, a_1, a_2, ...]`, this returns +`[a_0, [zeta]a_1, [zeta^2]a_2, a_3, [zeta]a_4, [zeta^2]a_5, a_6, ...]`, +where zeta is a cube root of unity in the multiplicative subgroup with +order (p - 1), i.e. zeta^3 = 1. + +`into_coset` should be set to `true` when moving into the coset, +and `false` when moving out. This toggles the choice of `zeta`. +``` + +### `poly/domain.rs:L357` (line) + +```text +Distribute powers to move into/from coset +``` + +### `poly/domain.rs:L371` (line) + +```text +Finish iFFT +``` + +### `poly/domain.rs:L377` (item-doc) + +```text +Get the size of the domain +``` + +### `poly/domain.rs:L382` (item-doc) + +```text +Get the size of the extended domain +``` + +### `poly/domain.rs:L387` (item-doc) + +```text +Get the size of the extended domain +``` + +### `poly/domain.rs:L392` (item-doc) + +```text +Get $\omega$, the generator of the $2^k$ order multiplicative subgroup. +``` + +### `poly/domain.rs:L397-L398` (item-doc) + +```text +Get $\omega^{-1}$, the inverse of the generator of the $2^k$ order +multiplicative subgroup. +``` + +### `poly/domain.rs:L403` (item-doc) + +```text +Get the generator of the extended domain's multiplicative subgroup. +``` + +### `poly/domain.rs:L408-L409` (item-doc) + +```text +Multiplies a value by some power of $\omega$, essentially rotating over +the domain. +``` + +### `poly/domain.rs:L420-L447` (item-doc) + +```text +Computes evaluations (at the point `x`, where `xn = x^n`) of Lagrange +polynomials `l_i(X)` defined such that `l_i(omega^i) = 1` and +`l_i(omega^j) = 0` for all `j != i` at each provided rotation `i`. + +# Implementation + +The polynomial + $$\prod_{j=0,j \neq i}^{n - 1} (X - \omega^j)$$ +has a root at all points in the domain except $\omega^i$, where it +evaluates to $$\prod_{j=0,j \neq i}^{n - 1} (\omega^i - +\omega^j)$$ and so we divide that polynomial by this value to obtain +$l_i(X)$. Since $$\prod_{j=0,j \neq i}^{n - 1} (X - \omega^j) + = \frac{X^n - 1}{X - \omega^i}$$ +then $l_i(x)$ for some $x$ is evaluated as + $$\left(\frac{x^n - 1}{x - \omega^i}\right) + \cdot \left(\frac{1}{\prod_{j=0,j \neq i}^{n - 1} (\omega^i - +\omega^j)}\right).$$ We refer to + $$1 \over \prod_{j=0,j \neq i}^{n - 1} (\omega^i - \omega^j)$$ +as the barycentric weight of $\omega^i$. + +We know that for $i = 0$ + $$\frac{1}{\prod_{j=0,j \neq i}^{n - 1} (\omega^i - \omega^j)} = +\frac{1}{n}.$$ + +If we multiply $(1 / n)$ by $\omega^i$ then we obtain + $$\frac{1}{\prod_{j=0,j \neq 0}^{n - 1} (\omega^i - \omega^j)} + = \frac{1}{\prod_{j=0,j \neq i}^{n - 1} (\omega^i - \omega^j)}$$ +which is the barycentric weight of $\omega^i$. +``` + +### `poly/domain.rs:L475` (item-doc) + +```text +Gets the quotient polynomial's degree (as a multiple of n) +``` + +### `poly/domain.rs:L480-L482` (item-doc) + +```text +Obtain a pinned version of this evaluation domain; a structure with the +minimal parameters needed to determine the rest of the evaluation +domain. +``` + +### `poly/domain.rs:L492` (item-doc) + +```text +Represents the minimal parameters that determine an `EvaluationDomain`. +``` + + +## `poly/kzg/mod.rs` + +### `poly/kzg/mod.rs:L1-L8` (module-doc) + +```text +We implement the multi-open technique developed in Halo 2. It is designed to +efficiently open multiple polynomials at multiple points while minimizing +proof size and verification time. In a nutshell, multiple opening queries +are batched into a single query by combining the target +polynomials/commitments and evaluation points using verifier-chosen +random scalars. + +For a more detailed explanation, see the [Halo 2 Book](https://zcash.github.io/halo2/design/proving-system/multipoint-opening.html) on Multipoint Openings. +``` + +### `poly/kzg/mod.rs:L17` (item-doc) + +```text +Multiscalar multiplication engines +``` + +### `poly/kzg/mod.rs:L19` (item-doc) + +```text +KZG commitment scheme +``` + +### `poly/kzg/mod.rs:L43` (item-doc) + +```text +Guard that restores the previous fewer-point-sets runtime setting when dropped. +``` + +### `poly/kzg/mod.rs:L68-L69` (item-doc) + +```text +No-op guard returned when the proof-system fewer-point-sets capability is +not compiled in. +``` + +### `poly/kzg/mod.rs:L74-L78` (item-doc) + +```text +Returns whether KZG multi-open dummy queries are currently enabled. + +With the `fewer-point-sets` feature compiled in, the default is `true` to +preserve the historical feature behavior. Call [`scoped_fewer_point_sets`] +to temporarily override it for a specific proof. +``` + +### `poly/kzg/mod.rs:L90-L94` (item-doc) + +```text +Temporarily enables or disables KZG multi-open dummy queries on this thread. + +This is intentionally scoped so recursive proving can use fewer point sets +for proofs verified inside a circuit while an outer proof in the same process +can be emitted without dummy query scalars. +``` + +### `poly/kzg/mod.rs:L133` (item-doc) + +```text +KZG verifier +``` + +### `poly/kzg/mod.rs:L185-L187` (item-doc) + +```text +Like [`inner_product`] but for coefficient-form polynomials that may +have different lengths (zero-extending the shorter operands via +[`Polynomial::padded_add`]). +``` + +### `poly/kzg/mod.rs:L204` (line) + +```text +Add dummy queries to reduce the number of distinct multi-open point sets. +``` + +### `poly/kzg/mod.rs:L222-L223` (line) + +```text +Refer to the halo2 book for docs: +https://zcash.github.io/halo2/design/proving-system/multipoint-opening.html +``` + +### `poly/kzg/mod.rs:L248-L255` (line) + +```text +Sort point sets by ascending cardinality to ensure the first set is the one +that contains fixed commitments (which are evaluated at x only). This +property is not necessary for the actual proving system, but it is important +for in-circuit verification of proofs. (It enables an optimization based on +an internal collapse.) + +The (len, i) key provides a deterministic total order even when two sets +share the same cardinality. +``` + +### `poly/kzg/mod.rs:L334` (line) + +```text +Add dummy queries to reduce the number of distinct multi-open point sets. +``` + +### `poly/kzg/mod.rs:L360-L361` (line) + +```text +Refer to the halo2 book for docs: +https://zcash.github.io/halo2/design/proving-system/multipoint-opening.html +``` + +### `poly/kzg/mod.rs:L408-L415` (line) + +```text +Sort point sets by ascending cardinality to ensure the first set is the one +that contains fixed commitments (which are evaluated at x only). This +property is not necessary for the actual proving system, but it is important +for in-circuit verification of proofs. (It enables an optimization based on +an internal collapse.) + +The (len, i) key provides a deterministic total order even when two sets +share the same cardinality. +``` + +### `poly/kzg/mod.rs:L452-L453` (line) + +```text +Sample a challenge x_3 for checking that f(X) was committed to +correctly. +``` + +### `poly/kzg/mod.rs:L468-L469` (line) + +```text +We can compute the expected msm_eval at x_3 using the u provided +by the prover and from x_2 +``` + +### `poly/kzg/mod.rs:L476` (line) + +```text +eval = (proof_eval - r_eval) / prod_i (x3 - point_i) +``` + +### `poly/kzg/mod.rs:L494-L495` (line) + +```text +Collapse all MSMs before combining with x4 powers, to match the +in-circuit verifier. Skip the first one since its x4 power is 1. +``` + +### `poly/kzg/mod.rs:L542` (line) + +```text +Scale zÏ€ - vG +``` + +### `poly/kzg/mod.rs:L552` (line) + +```text +(Ï€, C − vG + zÏ€) +``` + + +## `poly/kzg/msm.rs` + +### `poly/kzg/msm.rs:L26-L28` (item-doc) + +```text +A multi-scalar multiplication in the polynomial commitment scheme. +For every i, term (bases_i, scalars_i) may be have an optional +label_i for debugging or other purposes. +``` + +### `poly/kzg/msm.rs:L37` (item-doc) + +```text +Create an empty MSM instance +``` + +### `poly/kzg/msm.rs:L46` (item-doc) + +```text +Create an MSM from various MSMs +``` + +### `poly/kzg/msm.rs:L67` (item-doc) + +```text +Create a new MSM from a given base (with scalar of 1). +``` + +### `poly/kzg/msm.rs:L81-L88` (item-doc) + +```text +Evaluates the MSM to a single point and replaces all terms with that +single point (scalar = 1, label = `NoLabel`). + +This mirrors `AssignedMsm::collapse` in the circuits crate. + +# Panics (in debug mode) + +If any term carries a label other than `NoLabel` or `Advice`. +``` + +### `poly/kzg/msm.rs:L89-L93` (line) + +```text + +We only allow `NoLabel` or `Advice` because these types of labels are +not relevant for the `verifier_gadget` in `midnight-circuits` (at least for +now). Other types of labels may carry information that we do not want to lose +when "collapsing". +``` + +### `poly/kzg/msm.rs:L162` (item-doc) + +```text +Wrapper over the MSM function to use the blstrs underlying function +``` + +### `poly/kzg/msm.rs:L164` (line) + +```text +We remove zeros (keep only non-zero coefficients) +``` + +### `poly/kzg/msm.rs:L176-L177` (line) + +```text +We empirically checked that for MSMs larger than 2**18, the blstrs +implementation regresses. +``` + +### `poly/kzg/msm.rs:L179` (line) + +```text +Safe: we just checked type +``` + +### `poly/kzg/msm.rs:L193` (item-doc) + +```text +Two channel MSM accumulator +``` + +### `poly/kzg/msm.rs:L200` (item-doc) + +```text +A [DualMSM] split into left and right vectors of `(Scalar, Point)` tuples +``` + +### `poly/kzg/msm.rs:L240` (item-doc) + +```text +Create an empty two channel MSM accumulator instance +``` + +### `poly/kzg/msm.rs:L248` (item-doc) + +```text +Create a new two channel MSM accumulator instance +``` + +### `poly/kzg/msm.rs:L253` (item-doc) + +```text +Split the [DualMSM] into `left` and `right` +``` + +### `poly/kzg/msm.rs:L270` (item-doc) + +```text +Scale all scalars in the MSM by some scaling factor +``` + +### `poly/kzg/msm.rs:L276` (item-doc) + +```text +Add another multiexp into this one +``` + +### `poly/kzg/msm.rs:L282-L283` (item-doc) + +```text +Performs final pairing check with given verifier params and two channel +linear combination +``` + + +## `poly/kzg/params.rs` + +### `poly/kzg/params.rs:L17` (item-doc) + +```text +These are the public parameters for the polynomial commitment scheme. +``` + +### `poly/kzg/params.rs:L43` (item-doc) + +```text +Downsize the current parameters to match a smaller `k`. +``` + +### `poly/kzg/params.rs:L55-L63` (item-doc) + +```text +Recompute the Lagrange basis for a smaller circuit domain `new_k` while +keeping the full monomial basis `g` intact. + +Use this when the `single-h-commitment` feature is enabled: generate an +SRS large enough for the whole quotient polynomial (i.e. with `k'` +such that `2^{k'} ≥ (n-1) * quotient_poly_degree`), then call +`downsize_lagrange(k)` so that `max_k()` equals the circuit domain size +`k` while `g` retains its original length for the H-polynomial +commitment. +``` + +### `poly/kzg/params.rs:L74-L81` (item-doc) + +```text +Combine the monomial basis from `extended` with the Lagrange basis from +`self`, consuming both. This avoids the FFT that [`downsize_lagrange`] +would otherwise require. + +# Panics + +If `extended.g` is not strictly larger than `self.g`, or if the shared +prefix of the monomial bases does not match. +``` + +### `poly/kzg/params.rs:L96-L97` (item-doc) + +```text +Initializes parameters for the curve, draws toxic secret from given rng. +MUST NOT be used in production. +``` + +### `poly/kzg/params.rs:L99-L100` (line) + +```text +Largest root of unity exponent of the Engine is `2^E::Fr::S`, so we can +only support FFTs of polynomials below degree `2^E::Fr::S`. +``` + +### `poly/kzg/params.rs:L104` (line) + +```text +Calculate g = [G1, [s] G1, [s^2] G1, ..., [s^(n-1)] G1] in parallel. +``` + +### `poly/kzg/params.rs:L145-L146` (item-doc) + +```text +Initializes parameters for the curve through existing parameters +k, g, g_lagrange (optional), g2, s_g2 +``` + +### `poly/kzg/params.rs:L165` (item-doc) + +```text +Returns the committed lagrange polynomials of these KZG params. +``` + +### `poly/kzg/params.rs:L170` (item-doc) + +```text +Returns generator on G2 +``` + +### `poly/kzg/params.rs:L175` (item-doc) + +```text +Returns first power of secret on G2 +``` + +### `poly/kzg/params.rs:L180` (item-doc) + +```text +Writes parameters to buffer +``` + +### `poly/kzg/params.rs:L198` (item-doc) + +```text +Reads params from a buffer. +``` + +### `poly/kzg/params.rs:L252` (line) + +```text +avoid try branching for performance +``` + +### `poly/kzg/params.rs:L275-L277` (line) + +```text +TODO: see the issue at https://github.com/appliedzkp/halo2/issues/45 +So we probably need much smaller verifier key. However for new bases in g1 +should be in verifier keys. +``` + +### `poly/kzg/params.rs:L278` (item-doc) + +```text +KZG multi-open verification parameters +``` + +### `poly/kzg/params.rs:L290` (item-doc) + +```text +Writes parameters to buffer +``` + +### `poly/kzg/params.rs:L296` (item-doc) + +```text +Reads params from a buffer. +``` + +### `poly/kzg/params.rs:L311-L312` (item-doc) + +```text +Consume the prover parameters into verifier parameters. Need to specify +the size of public inputs. +``` + + +## `poly/kzg/utils.rs` + +### `poly/kzg/utils.rs:L40-L41` (line) + +```text +Construct sets of unique commitments and corresponding information about +their queries. +``` + +### `poly/kzg/utils.rs:L44-L45` (line) + +```text +Also construct mapping from a unique point to a point_index. This defines +an ordering on the points. +``` + +### `poly/kzg/utils.rs:L48-L49` (line) + +```text +Iterate over all of the queries, computing the ordering of the points +while also creating new commitment data. +``` + +### `poly/kzg/utils.rs:L68` (line) + +```text +Also construct inverse mapping from point_index to the point +``` + +### `poly/kzg/utils.rs:L72` (line) + +```text +Construct map of unique ordered point_idx_sets to their set_idx +``` + +### `poly/kzg/utils.rs:L74` (line) + +```text +Also construct mapping from commitment to point_idx_set +``` + +### `poly/kzg/utils.rs:L80` (line) + +```text +Push point_index_set to CommitmentData for the relevant commitment +``` + +### `poly/kzg/utils.rs:L87` (line) + +```text +Initialise empty evals vec for each unique commitment +``` + +### `poly/kzg/utils.rs:L93` (line) + +```text +Populate set_index, evals and points for each commitment using point_idx_sets +``` + +### `poly/kzg/utils.rs:L95` (line) + +```text +The index of the point at which the commitment is queried +``` + +### `poly/kzg/utils.rs:L98` (line) + +```text +The point_index_set at which the commitment was queried +``` + +### `poly/kzg/utils.rs:L106` (line) + +```text +The set_index of the point_index_set +``` + +### `poly/kzg/utils.rs:L111` (line) + +```text +The offset of the point_index in the point_index_set +``` + +### `poly/kzg/utils.rs:L117` (line) + +```text +Insert the eval using the ordering of the point_index_set +``` + +### `poly/kzg/utils.rs:L123` (line) + +```text +Get actual points in each point set +``` + +### `poly/kzg/utils.rs:L135-L149` (item-doc) + +```text +Computes the dummy openings needed to reduce the number of distinct +multi-open point sets. Each input `(key, point)` pair represents a query +(e.g., a commitment opened at a given point). The function groups queries +by key (by commitment) computes the union of all point sets that contain +more than one point, and returns the missing `(index, point)` pairs that, +once added, make every such point set identical. Keys with a single point +are left untouched (we do this because there are many commitments opened +at a single point, e.g. most selectors; we could also pad those, but the +impact on the proof size would be more significant). + +Each returned `index` refers to the position of the key's first occurrence +in the input, so callers can index back into the original query slice. + +The output order is deterministic (insertion order), so prover and verifier +stay in sync. +``` + +### `poly/kzg/utils.rs:L153` (line) + +```text +Group by key, tracking each key's first occurrence index. +``` + +### `poly/kzg/utils.rs:L163` (line) + +```text +Union of all non-singleton point sets (insertion order). +``` + +### `poly/kzg/utils.rs:L177` (line) + +```text +Collect missing (first_index, point) dummy queries. +``` + + +## `poly/mod.rs` + +### `poly/mod.rs:L1-L3` (module-doc) + +```text +Contains utilities for performing arithmetic over univariate polynomials in +various forms, including computing commitments to them and provably opening +the committed polynomials at arbitrary points. +``` + +### `poly/mod.rs:L23` (item-doc) + +```text +KZG commitment scheme +``` + +### `poly/mod.rs:L33` (item-doc) + +```text +This is an error that could occur during proving or circuit synthesis. +``` + +### `poly/mod.rs:L34` (line) + +```text +TODO: these errors need to be cleaned up +``` + +### `poly/mod.rs:L37` (item-doc) + +```text +OpeningProof is not well-formed +``` + +### `poly/mod.rs:L39` (item-doc) + +```text +Caller needs to re-sample a point +``` + +### `poly/mod.rs:L41-L42` (item-doc) + +```text +Multiopen argument only supports a single query to the same (commitment, +opening) pair. +``` + +### `poly/mod.rs:L46` (item-doc) + +```text +The representation with which a polynomial is encoded. +``` + +### `poly/mod.rs:L48-L49` (item-doc) + +```text +Computes the number of field elements needed to encode a polynomial +in this representation for a given evaluation domain. +``` + +### `poly/mod.rs:L52-L53` (item-doc) + +```text +Constructs an empty (zero) polynomial in this representation, +appropriate for the given domain. +``` + +### `poly/mod.rs:L63` (item-doc) + +```text +Returns the generator `ω` associated with the evaluation domain. +``` + +### `poly/mod.rs:L66-L67` (item-doc) + +```text +Returns the logarithmic size parameter `k` of the evaluation domain, +where the domain size is `2^k`. +``` + +### `poly/mod.rs:L70-L71` (item-doc) + +```text +Converts a polynomial from coefficient form into this representation +over the given evaluation domain. +``` + +### `poly/mod.rs:L77-L78` (item-doc) + +```text +Returns the multiplicative coset generator `g_coset` if this +representation uses an extended domain. +``` + +### `poly/mod.rs:L82` (item-doc) + +```text +The polynomial is defined as coefficients +``` + +### `poly/mod.rs:L110` (item-doc) + +```text +The polynomial is defined as coefficients of Lagrange basis polynomials +``` + +### `poly/mod.rs:L138-L139` (item-doc) + +```text +The polynomial is defined as coefficients of Lagrange basis polynomials in +an extended size domain which supports multiplication +``` + +### `poly/mod.rs:L167-L168` (item-doc) + +```text +Represents a univariate polynomial defined over a field and a particular +representation. +``` + +### `poly/mod.rs:L176` (item-doc) + +```text +Creates a zero polynomial of the given size. +``` + +### `poly/mod.rs:L186-L193` (item-doc) + +```text +Adds two coefficient-form polynomials that may have different lengths, +zero-extending the shorter one. Returns a polynomial whose length is +`max(self.len(), rhs.len())`. + +This is only meaningful for coefficient-form (`Coeff`) polynomials, +where trailing zeros do not change the represented polynomial. In +Lagrange or ExtendedLagrange representations the length is tied to the +evaluation domain, so mixing lengths would be semantically wrong. +``` + +### `poly/mod.rs:L208-L215` (item-doc) + +```text +Subtracts two coefficient-form polynomials that may have different +lengths, zero-extending the shorter one. Returns a polynomial whose +length is `max(self.len(), rhs.len())`. + +This is only meaningful for coefficient-form (`Coeff`) polynomials, +where trailing zeros do not change the represented polynomial. In +Lagrange or ExtendedLagrange representations the length is tied to the +evaluation domain, so mixing lengths would be semantically wrong. +``` + +### `poly/mod.rs:L288-L289` (item-doc) + +```text +Iterate over the values, which are either in coefficient or evaluation +form depending on the representation `B`. +``` + +### `poly/mod.rs:L294-L295` (item-doc) + +```text +Iterate over the values mutably, which are either in coefficient or +evaluation form depending on the representation `B`. +``` + +### `poly/mod.rs:L300-L301` (item-doc) + +```text +Gets the size of this polynomial in terms of the number of +coefficients used to describe it. +``` + +### `poly/mod.rs:L308` (item-doc) + +```text +Reads polynomial from buffer using `SerdePrimeField::read`. +``` + +### `poly/mod.rs:L323` (item-doc) + +```text +Writes polynomial to buffer using `SerdePrimeField::write`. +``` + +### `poly/mod.rs:L345-L346` (line) + +```text +If the denominator is trivial, we can skip it, reducing the +size of the batch inversion. +``` + +### `poly/mod.rs:L376-L381` (item-doc) + +```text +Point-wise addition of two polynomials of the **same length**. + +Both operands must have been created over the same domain (or with the same +number of coefficients). This invariant is enforced by a runtime assertion. +For coefficient-form polynomials of different lengths see +`Polynomial::padded_add`. +``` + +### `poly/mod.rs:L391-L396` (item-doc) + +```text +Point-wise addition-assignment of two polynomials of the **same length**. + +Both operands must have been created over the same domain (or with the same +number of coefficients). This invariant is enforced by a runtime assertion. +For coefficient-form polynomials of different lengths see +`Polynomial::padded_add`. +``` + +### `poly/mod.rs:L410-L415` (item-doc) + +```text +Point-wise addition of two polynomials of the **same length** (by value). + +Both operands must have been created over the same domain (or with the same +number of coefficients). This invariant is enforced by a runtime assertion. +For coefficient-form polynomials of different lengths see +`Polynomial::padded_add`. +``` + +### `poly/mod.rs:L431-L436` (item-doc) + +```text +Point-wise subtraction of two polynomials of the **same length**. + +Both operands must have been created over the same domain (or with the same +number of coefficients). This invariant is enforced by a runtime assertion. +For coefficient-form polynomials of different lengths see +`Polynomial::padded_sub`. +``` + +### `poly/mod.rs:L453` (item-doc) + +```text +Rotates the values in a `LagrangeCoeff` polynomial by `Rotation` +``` + +### `poly/mod.rs:L505-L507` (item-doc) + +```text +Describes the relative rotation of a vector. Negative numbers represent +reverse (leftmost) rotations and positive numbers represent forward +(rightmost) rotations. Zero represents no rotation. +``` + +### `poly/mod.rs:L512` (item-doc) + +```text +The current location in the evaluation domain +``` + +### `poly/mod.rs:L517` (item-doc) + +```text +The previous location in the evaluation domain +``` + +### `poly/mod.rs:L522` (item-doc) + +```text +The next location in the evaluation domain +``` + + +## `poly/query.rs` + +### `poly/query.rs:L10` (item-doc) + +```text +A structured label for polynomial commitments in verifier queries. +``` + +### `poly/query.rs:L13` (item-doc) + +```text +Advice column commitment (column index). +``` + +### `poly/query.rs:L15` (item-doc) + +```text +Instance column commitment (column index). +``` + +### `poly/query.rs:L17` (item-doc) + +```text +Fixed column commitment (column index). +``` + +### `poly/query.rs:L19` (item-doc) + +```text +Permutation verifying-key commitment (index). +``` + +### `poly/query.rs:L21` (item-doc) + +```text +User-defined label. +``` + +### `poly/query.rs:L23` (item-doc) + +```text +No label. +``` + +### `poly/query.rs:L50` (item-doc) + +```text +A polynomial query at a point +``` + +### `poly/query.rs:L53` (item-doc) + +```text +Point at which polynomial is queried +``` + +### `poly/query.rs:L55` (item-doc) + +```text +Coefficients of polynomial +``` + +### `poly/query.rs:L63` (item-doc) + +```text +Create a new prover query based on a polynomial +``` + +### `poly/query.rs:L100-L111` (item-doc) + +```text +A reference to a polynomial commitment. + +A commitment can either be a single piece (`OnePiece`) or a linear +combination of commitments (`Linear`). The linear form is used, e.g., for +the linearization polynomial, whose commitment is expressed as: + * scalars (representing - partially or fully - evaluated identities), + and + * commitments (representing, either, commitments to simple, + multiplicative selectors or the commitment to the constant polynomial + `P(X) = 1`). +The linear combination is given in form of two vectors: one holds references +to the commitments, the other one holds the scalars. +``` + +### `poly/query.rs:L136-L147` (item-doc) + +```text +Returns the commitment as a list of `(scalar, commitment)` pairs. + +If the commitment is represented in one piece, this function returns +`vec![(F::ONE, com)]`. + +If the commitment is a linear combination, this function returns the +pairs `(scalar_i, commitment_i)`. + +# Panics + +If the commitment is "linear" and the number of points and the number +of scalars are not equal. +``` + +### `poly/query.rs:L166` (item-doc) + +```text +A polynomial query at a point. +``` + +### `poly/query.rs:L169` (item-doc) + +```text +Point at which polynomial is queried. +``` + +### `poly/query.rs:L171` (item-doc) + +```text +Optional label identifying the commitment in this query. +``` + +### `poly/query.rs:L173` (item-doc) + +```text +Commitment to polynomial. +``` + +### `poly/query.rs:L175` (item-doc) + +```text +Evaluation of polynomial at query point. +``` + +### `poly/query.rs:L184` (item-doc) + +```text +Create a new verifier query based on an optionally labeled commitment. +``` + +### `poly/query.rs:L199-L207` (item-doc) + +```text +Create a new verifier query based on a commitment +represented in the form of curve points and corresponding +scalars. Each term carries its own `CommitmentLabel` so that +downstream consumers (e.g. `from_dual_msm`) can classify +individual bases correctly. + +# panics + +If the number of points, scalars, or base_labels differs. +``` + + +## `transcript/implementors.rs` + +### `transcript/implementors.rs:L79` (item-doc) + +```text +Converts it to compressed form in bytes +``` + +### `transcript/implementors.rs:L128-L130` (line) + +```text +////////////////////////////////////////////////////////// +/// Implementation of Hashable for BLS12-381 with Blake // +////////////////////////////////////////////////////////// +``` + +### `transcript/implementors.rs:L133` (item-doc) + +```text +Converts it to compressed form in bytes +``` + +### `transcript/implementors.rs:L181-L183` (line) + +```text +///////////////////////////////////////////////////////////////////// +/// Implementation of TranscriptHash for Keccak256 // +///////////////////////////////////////////////////////////////////// +``` + +### `transcript/implementors.rs:L199-L201` (line) + +```text +Keccak256 produces a 32-byte digest. For Fiat-Shamir challenges we +sample from that digest directly as a big-endian integer modulo the +scalar-field modulus. +``` + +### `transcript/implementors.rs:L204-L205` (line) + +```text +Re-seed the state with the squeezed challenge so that subsequent +absorb/squeeze calls depend on the challenge we just produced. +``` + +### `transcript/implementors.rs:L265-L283` (item-doc) + +```text +Converts the point to the EIP-2537 padded uncompressed form for +Fiat-Shamir absorbtion: 128 bytes laid out as + + x_hi (32) || x_lo (32) || y_hi (32) || y_lo (32) + +where each coord is 16 zero pad bytes followed by 48 big-endian +bytes of the BLS12-381 base-field element. The identity point +is encoded as 128 zero bytes (matches the EIP-2537 (0, 0) +convention rather than the BLS-spec 0x40-leading-byte form). + +This format lets the EVM verifier (`Halo2Verifier.sol:: +common_g1_uncompressed`) skip the 384-bit sign-bit ladder it +would need if we were hashing the 48-byte compressed encoding; +the EVM can just memcpy the calldata words verbatim into the +keccak buffer (after masking the 16-byte zero pads to defeat +transcript malleability). + +The wire format (`to_bytes`) is unchanged — points are still +transmitted in the 48-byte compressed encoding. +``` + +### `transcript/implementors.rs:L288-L292` (line) + +```text +`UncompressedEncoding::to_uncompressed` returns a wrapper +around 96 bytes: x_be(48) || y_be(48). For non-identity +points, the top 3 bits of byte 0 are zero (since +x < p < 2^381), so we copy the bytes verbatim into the +EIP-2537 64-byte slots. +``` + + +## `transcript/mod.rs` + +### `transcript/mod.rs:L1-L2` (module-doc) + +```text +This module contains utilities and traits for dealing with Fiat-Shamir +transcripts. +``` + +### `transcript/mod.rs:L9` (item-doc) + +```text +Prefix to a prover's message soliciting a challenge +``` + +### `transcript/mod.rs:L12` (item-doc) + +```text +Prefix to a prover's message +``` + +### `transcript/mod.rs:L15-L18` (item-doc) + +```text +A transcript hash input that can be rendered as bytes for diagnostics. + +The byte representation is used only by verifier trace hooks; the +transcript itself continues to absorb the native `Input` type. +``` + +### `transcript/mod.rs:L20` (item-doc) + +```text +Convert the input into a deterministic diagnostic byte string. +``` + +### `transcript/mod.rs:L40` (item-doc) + +```text +Hash function that can be used for transcript +``` + +### `transcript/mod.rs:L42` (item-doc) + +```text +Input type of the hash function +``` + +### `transcript/mod.rs:L44` (item-doc) + +```text +Output type of the hash function +``` + +### `transcript/mod.rs:L47` (item-doc) + +```text +Initialise the hasher +``` + +### `transcript/mod.rs:L49` (item-doc) + +```text +Absorb an element +``` + +### `transcript/mod.rs:L51` (item-doc) + +```text +Squeeze an output +``` + +### `transcript/mod.rs:L55` (item-doc) + +```text +Traits to represent values that can be hashed with a `TranscriptHash` +``` + +### `transcript/mod.rs:L57` (item-doc) + +```text +Converts the hashable type to a format that can be hashed with `H` +``` + +### `transcript/mod.rs:L60` (item-doc) + +```text +Converts the hashable type to bytes to be added to the transcript. +``` + +### `transcript/mod.rs:L63` (item-doc) + +```text +Reads bytes from a buffer and returns `Self`. +``` + +### `transcript/mod.rs:L67` (item-doc) + +```text +Trait to represent values that can be sampled from a `TranscriptHash` +``` + +### `transcript/mod.rs:L69` (item-doc) + +```text +Converts `H`'s output to Self +``` + +### `transcript/mod.rs:L73` (item-doc) + +```text +Generic transcript view +``` + +### `transcript/mod.rs:L75` (item-doc) + +```text +Hash function +``` + +### `transcript/mod.rs:L78` (item-doc) + +```text +Initialises the transcript +``` + +### `transcript/mod.rs:L81` (item-doc) + +```text +Parses an existing transcript +``` + +### `transcript/mod.rs:L84-L85` (item-doc) + +```text +Squeeze a challenge of type `T`, which only needs to be `Sampleable` +with the corresponding hash function. +``` + +### `transcript/mod.rs:L88-L89` (item-doc) + +```text +Writing a hashable element `T` to the transcript without writing it to +the proof, treating it as a common commitment. +``` + +### `transcript/mod.rs:L92` (item-doc) + +```text +Read a hashable element `T` from the prover. +``` + +### `transcript/mod.rs:L95` (item-doc) + +```text +Write a hashable element `T` to the proof and the transcript. +``` + +### `transcript/mod.rs:L98` (item-doc) + +```text +Returns the buffer with the proof. +``` + +### `transcript/mod.rs:L101-L103` (item-doc) + +```text +Checks that the transcript is empty. +This is used to make sure a transcript does not contain trailing bytes +at the end of a proof verification. +``` + +### `transcript/mod.rs:L108` (item-doc) + +```text +Transcript used in proofs, parametrised by its hash function. +``` + +### `transcript/mod.rs:L115-L116` (item-doc) + +```text +Returns the buffer for non default reading of the buffer (such as for +reading an empty proof) +``` + + +## `utils/arithmetic.rs` + +### `utils/arithmetic.rs:L1-L2` (module-doc) + +```text +This module provides common utilities, traits and structures for group, +field and polynomial arithmetic. +``` + +### `utils/arithmetic.rs:L18-L20` (item-doc) + +```text +This represents an element of a group with basic operations that can be +performed. This allows an FFT implementation (for example) to operate +generically over either a field or elliptic curve group. +``` + +### `utils/arithmetic.rs:L33` (item-doc) + +```text +Convert coefficient bases group elements to lagrange basis by inverse FFT. +``` + +### `utils/arithmetic.rs:L52` (item-doc) + +```text +This evaluates a provided polynomial (in coefficient form) at `point`. +``` + +### `utils/arithmetic.rs:L78-L80` (item-doc) + +```text +This computes the inner product of two vectors `a` and `b`. + +This function will panic if the two vectors are not the same size. +``` + +### `utils/arithmetic.rs:L82` (line) + +```text +TODO: parallelize? +``` + +### `utils/arithmetic.rs:L93-L94` (item-doc) + +```text +Divides polynomial `a` in `X` by `X - b` with +no remainder. +``` + +### `utils/arithmetic.rs:L116-L117` (item-doc) + +```text +This utility function will parallelize an operation that is to be +performed over a mutable slice. +``` + +### `utils/arithmetic.rs:L119-L140` (line) + +```text +Algorithm rationale: + +Using the stdlib `chunks_mut` will lead to severe load imbalance. +From https://github.com/rust-lang/rust/blob/e94bda3/library/core/src/slice/iter.rs#L1607-L1637 +if the division is not exact, the last chunk will be the remainder. + +Dividing 40 items on 12 threads will lead to a chunk size of 40/12 = 3, +There will be a 13 chunks of size 3 and 1 of size 1 distributed on 12 +threads. This leads to 1 thread working on 6 iterations, 1 on 4 +iterations and 10 on 3 iterations, a load imbalance of 2x. + +Instead we can divide work into chunks of size +4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3 = 4*4 + 3*8 = 40 + +This would lead to a 6/4 = 1.5x speedup compared to naive chunks_mut + +See also OpenMP spec (page 60) +http://www.openmp.org/mp-documents/openmp-4.5.pdf +"When no chunk_size is specified, the iteration space is divided into chunks +that are approximately equal in size, and at most one chunk is distributed to +each thread. The size of the chunks is unspecified in this case." +This implies chunks are the same size ±1 +``` + +### `utils/arithmetic.rs:L151-L152` (line) + +```text +Skip special-case: number of iterations is cleanly divided by number of +threads. +``` + +### `utils/arithmetic.rs:L159` (line) + +```text +Skip special-case: less iterations than number of threads. +``` + +### `utils/arithmetic.rs:L169-L171` (item-doc) + +```text +Returns coefficients of an n - 1 degree polynomial given a set of n points +and their evaluations. This function will panic if two values in `points` +are the same. +``` + +### `utils/arithmetic.rs:L181` (line) + +```text +Constant polynomial +``` + +### `utils/arithmetic.rs:L192` (line) + +```text +Compute (x_j - x_k)^(-1) for each j != i +``` + +### `utils/arithmetic.rs:L231-L245` (item-doc) + +```text +Truncates a scalar field element to half its byte size. + +This function reduces a scalar field element `scalar` to half its size by +retaining only the lower half of its little-endian byte representation. + +# Note +For cryptographically secure elliptic curves, the scalar field is +approximately twice the size of the security parameter. When scalars are +sampled uniformly at random, truncating to half the field size retains +sufficient entropy for security while reducing computational overhead. + +# Warning +128 bits may not be enough entropy depending on the application. For +example, it makes a collision attack feasible with 2^64 memory and ~2^64 +operations. +``` + +### `utils/arithmetic.rs:L301-L305` (item-doc) + +```text +Computes the inner product of a set of polynomial evaluations and a set of +scalar values. This function computes the weighted sum of polynomial +evaluations. Each vector in `evals_set` is multiplied element-wise by a +corresponding scalar from `scalars`, and the results are accumulated +into a single vector. +``` + +### `utils/arithmetic.rs:L319` (item-doc) + +```text +Multi scalar multiplication engine +``` + +### `utils/arithmetic.rs:L321` (item-doc) + +```text +Add arbitrary term (the scalar and the point). +``` + +### `utils/arithmetic.rs:L324` (item-doc) + +```text +Add another multiexp into this one. +``` + +### `utils/arithmetic.rs:L327` (item-doc) + +```text +Scale all scalars in the MSM by some scaling factor. +``` + +### `utils/arithmetic.rs:L330` (item-doc) + +```text +Perform multiexp and check that it results in zero. +``` + +### `utils/arithmetic.rs:L333` (item-doc) + +```text +Perform multiexp and return the result. +``` + +### `utils/arithmetic.rs:L336` (item-doc) + +```text +Return base points. +``` + +### `utils/arithmetic.rs:L339` (item-doc) + +```text +Scalars. +``` + +### `utils/arithmetic.rs:L342` (item-doc) + +```text +Base labels. +``` + + +## `utils/helpers.rs` + +### `utils/helpers.rs:L1` (module-doc) + +```text +HELPER FUNCTIONS +``` + +### `utils/helpers.rs:L14` (item-doc) + +```text +This enum specifies how various types are serialized and deserialized. +``` + +### `utils/helpers.rs:L17-L19` (item-doc) + +```text +Curve elements are serialized in compressed form. +Field elements are serialized in standard form, with endianness +specified by the `PrimeField` implementation. +``` + +### `utils/helpers.rs:L21-L24` (item-doc) + +```text +Curve elements are serialized in uncompressed form. Field elements are +serialized in their internal Montgomery representation. +When deserializing, checks are performed to ensure curve elements indeed +lie on the curve and field elements are less than modulus. +``` + +### `utils/helpers.rs:L26` (item-doc) + +```text +Serialization is the same as `RawBytes`, but no checks are performed. +``` + +### `utils/helpers.rs:L30` (item-doc) + +```text +Interface for Serde objects that can be represented in compressed form. +``` + +### `utils/helpers.rs:L32-L37` (item-doc) + +```text +Reads an element from the buffer and parses it according to the +`format`: +- `Processed`: Reads a compressed element and decompress it +- `RawBytes`: Reads an uncompressed element and checks its correctness +- `RawBytesUnchecked`: Reads an uncompressed element without performing + any checks +``` + +### `utils/helpers.rs:L40-L42` (item-doc) + +```text +Writes an element according to `format`: +- `Processed`: Writes a compressed element +- Otherwise: Writes an uncompressed element +``` + +### `utils/helpers.rs:L46` (item-doc) + +```text +Byte length of an affine curve element according to `format`. +``` + +### `utils/helpers.rs:L54-L55` (item-doc) + +```text +Helper function to read a field element with a serde format. There is no way +to compress field elements, so `Processed` and `RawBytes` act equivalently. +``` + +### `utils/helpers.rs:L67` (item-doc) + +```text +Trait for serialising SerdeObjects +``` + +### `utils/helpers.rs:L73-L80` (item-doc) + +```text +Reads an element from the buffer and parses it according to the +`format`: +- `Processed`: Reads a compressed curve element and decompress it +- `RawBytes`: Reads an uncompressed curve element with coordinates in + Montgomery form. Checks that field elements are less than modulus, and + then checks that the point is on the curve. +- `RawBytesUnchecked`: Reads an uncompressed curve element with + coordinates in Montgomery form; does not perform any checks +``` + +### `utils/helpers.rs:L100-L103` (item-doc) + +```text +Writes a curve element according to `format`: +- `Processed`: Writes a compressed curve element +- Otherwise: Writes an uncompressed curve element with coordinates in + Montgomery form +``` + +### `utils/helpers.rs:L112-L114` (item-doc) + +```text +Convert a slice of `bool` into a `u8`. + +Panics if the slice has length greater than 8. +``` + +### `utils/helpers.rs:L124` (item-doc) + +```text +Writes the first `bits.len()` bits of a `u8` into `bits`. +``` + +### `utils/helpers.rs:L131` (item-doc) + +```text +Reads a vector of polynomials from buffer +``` + +### `utils/helpers.rs:L145` (item-doc) + +```text +Writes a slice of polynomials to buffer +``` + +### `utils/helpers.rs:L157-L158` (item-doc) + +```text +Gets the total number of bytes of a slice of polynomials, assuming all +polynomials are the same length +``` + + +## `utils/mod.rs` + +### `utils/mod.rs:L1` (module-doc) + +```text +The utils module contains small, reusable functions +``` + + +## `utils/multicore.rs` + +### `utils/multicore.rs:L10-L15` (item-doc) + +```text +Implements `iter.try_fold().try_reduce()` for `rayon::iter::ParallelIterator`, +falling back on `Iterator::try_fold` when the `multicore` feature flag is +disabled. +The `try_fold_and_reduce` function can only be called by a iter with +`Result` item type because the `fold_op` must meet the trait +bounds of both `try_fold` and `try_reduce` from rayon. +``` + + +## `utils/rational.rs` + +### `utils/rational.rs:L1-L2` (module-doc) + +```text +Module that implements a rational value. A rational value is stored as a +fraction, so the backend can use batch inversion. +``` + +### `utils/rational.rs:L7-L9` (item-doc) + +```text +A rational value. + +A denominator of zero maps to a value of zero. +``` + +### `utils/rational.rs:L12` (item-doc) + +```text +The field element zero. +``` + +### `utils/rational.rs:L14` (item-doc) + +```text +A value that does not require inversion to evaluate. +``` + +### `utils/rational.rs:L16` (item-doc) + +```text +A value stored as a fraction to enable batch inversion. +``` + +### `utils/rational.rs:L47` (line) + +```text +At least one side is directly zero. +``` + +### `utils/rational.rs:L51` (line) + +```text +One side is x/0 which maps to zero. +``` + +### `utils/rational.rs:L58` (line) + +```text +Okay, we need to do some actual math... +``` + +### `utils/rational.rs:L96` (line) + +```text +One side is directly zero. +``` + +### `utils/rational.rs:L100` (line) + +```text +One side is x/0 which maps to zero. +``` + +### `utils/rational.rs:L107` (line) + +```text +Okay, we need to do some actual math... +``` + +### `utils/rational.rs:L280` (item-doc) + +```text +Returns the numerator. +``` + +### `utils/rational.rs:L289` (item-doc) + +```text +Returns the denominator, if non-trivial. +``` + +### `utils/rational.rs:L298` (item-doc) + +```text +Returns true iff this element is zero. +``` + +### `utils/rational.rs:L303` (line) + +```text +Assigned maps x/0 -> 0. +``` + +### `utils/rational.rs:L310` (item-doc) + +```text +Doubles this element. +``` + +### `utils/rational.rs:L322` (item-doc) + +```text +Squares this element. +``` + +### `utils/rational.rs:L334` (item-doc) + +```text +Cubes this element. +``` + +### `utils/rational.rs:L340` (item-doc) + +```text +Inverts this assigned value (taking the inverse of zero to be zero). +``` + +### `utils/rational.rs:L349-L352` (item-doc) + +```text +Evaluates this assigned value directly, performing an unbatched +inversion if necessary. + +If the denominator is zero, this returns zero. +``` + +### `utils/rational.rs:L374` (line) + +```text +We use (numerator, denominator) in the comments below to denote a rational. +``` + +### `utils/rational.rs:L377-L378` (line) + +```text +a = 2 +b = (1,0) +``` + +### `utils/rational.rs:L382-L383` (line) + +```text +2 + (1,0) = 2 + 0 = 2 +This fails if addition is implemented using normal rules for rationals. +``` + +### `utils/rational.rs:L390-L391` (line) + +```text +a = (1,2) +b = (1,0) +``` + +### `utils/rational.rs:L395-L396` (line) + +```text +(1,2) + (1,0) = (1,2) + 0 = (1,2) +This fails if addition is implemented using normal rules for rationals. +``` + +### `utils/rational.rs:L403-L404` (line) + +```text +a = 2 +b = (1,0) +``` + +### `utils/rational.rs:L408-L409` (line) + +```text +(1,0) - 2 = 0 - 2 = -2 +This fails if subtraction is implemented using normal rules for rationals. +``` + +### `utils/rational.rs:L412` (line) + +```text +2 - (1,0) = 2 - 0 = 2 +``` + +### `utils/rational.rs:L418-L419` (line) + +```text +a = (1,2) +b = (1,0) +``` + +### `utils/rational.rs:L423-L424` (line) + +```text +(1,0) - (1,2) = 0 - (1,2) = -(1,2) +This fails if subtraction is implemented using normal rules for rationals. +``` + +### `utils/rational.rs:L427` (line) + +```text +(1,2) - (1,0) = (1,2) - 0 = (1,2) +``` + +### `utils/rational.rs:L433-L434` (line) + +```text +a = (1,2) +b = (1,0) +``` + +### `utils/rational.rs:L438` (line) + +```text +(1,2) * (1,0) = (1,2) * 0 = 0 +``` + +### `utils/rational.rs:L441` (line) + +```text +(1,0) * (1,2) = 0 * (1,2) = 0 +``` + +### `utils/rational.rs:L565` (item-doc) + +```text +Use narrow that can be easily reduced. +``` + +### `utils/rational.rs:L578` (item-doc) + +```text +Generates half of the denominators as zero to represent a deferred inversion. +``` + +### `utils/rational.rs:L613-L615` (line) + +```text +Ensure that: +- we have at least one value to apply unary operators to. +- we can apply every binary operator pairwise sequentially. +``` + +### `utils/rational.rs:L626` (line) + +```text +Evaluate the values at the start. +``` + +### `utils/rational.rs:L629` (line) + +```text +Apply the operations to both the deferred and evaluated values. +``` + +### `utils/rational.rs:L636-L637` (line) + +```text +Process all binary operators. We are guaranteed to have exactly as many +binary operators as we need calls to the reduction closure. +``` + +### `utils/rational.rs:L646-L649` (line) + +```text +Process any unary operators that weren't handled in the reduce() call +above (either if we only had one item, or there were unary operators +after the last binary operator). We are guaranteed to have no binary +operators remaining at this point. +``` + +### `utils/rational.rs:L661-L662` (line) + +```text +The two should be equal, i.e. deferred inversion should commute with the +list of operations. +``` diff --git a/proofs/solidity-verifier/docs/reference/PORTING_NOTES.md b/proofs/solidity-verifier/docs/reference/PORTING_NOTES.md new file mode 100644 index 000000000..be1279d85 --- /dev/null +++ b/proofs/solidity-verifier/docs/reference/PORTING_NOTES.md @@ -0,0 +1,195 @@ +# Historical BN254 -> BLS12-381 Porting Notes + +This document is a historical snapshot from the early BLS12-381 port. It is not +the current API reference. + +This branch (`bls`) is a **work-in-progress** port of the Halo2 Solidity +verifier from BN254 to BLS12-381 with the EIP-2537 precompiles. Read this +document **before** trying to use the generated Solidity for anything other +than reviewing its shape -- there are open ends. + +## Goals + +1. Replace BN254 (precompiles `0x06`, `0x07`, `0x08`) with BLS12-381 EIP-2537 + precompiles (`0x0b`, `0x0c`, `0x0f`). +2. Switch all field/curve constants and calldata layouts to BLS12-381. +3. Keep the codegen pipeline driving the Rust crate working so we can iterate + on the verifier template, calldata encoding, and tests. + +## Current state + +| Layer | State | +|----------------------------------------|---------------------------------------------------------| +| `templates/contracts/Halo2Verifier.sol` | Rewritten for BLS12-381 / EIP-2537 | +| `templates/contracts/Halo2VerifyingKey.sol` | Rewritten for 4-word G1 / EIP-2537 layout | +| `src/lowering/template.rs` | `G1Words = (U256;4)`; VK length doubled | +| `src/lowering/util.rs` | EcPoint{base: Ptr} 4-word stride; Value signed offsets | +| `src/lowering/pcs.rs` | Rewritten as an EIP-2537 Yul emitter | +| `src/lowering.rs` | Emits BLS-shape constants + `proof_to_bls_padded` | +| `src/evm.rs` | revm 19, Prague spec, EIP-2537 precompiles (commit f028b99) | +| `SolidityGenerator::encode_calldata` | Current shape-checked calldata helper | +| `src/transcript.rs` | **Unchanged** -- still BN254-shape | +| `src/test.rs` heavy tests | `#[ignore]`'d (need BLS prover) | +| `vendor/halo2/` (uncommitted) | v0.4 trimmed to 4 sub-crates + 4 surgical patches | +| Cargo.toml `halo2_proofs` dep | Still v0.3 (pre-vendor switch) | +| Generated artifacts under `generated/` | Stale (BN254 outputs from `main`) | + +Stages landed: + +* **Stage A** (commit f028b99): revm 3.5 -> 19, Prague spec, EIP-2537 + precompiles wired in, plus a hand-assembled `prague_evm_runs_eip2537_g1add_to_identity` + smoke test that calls `0x0b` with `2 * G1` and asserts non-zero output. +* **Stage B** (commit c041bcb): the predecessor of + `SolidityGenerator::repack_proof()` + walks the proof byte-stream using `ConstraintSystemMeta` and the + scheme's `num_trailing_g1_points`, expanding each G1 from 64 -> 128 + bytes to match what the BLS Solidity verifier reads. The current public + helper is `SolidityGenerator::encode_calldata`. `examples/separately.rs` now + deploys both contracts on the Prague EVM and reports outcome + categorically (currently: revert at pairing with ~100k gas, which + confirms the precompiles execute). + +Stage C (in progress, not yet wired into Cargo.toml): + +* `vendor/halo2/` carries a trimmed copy of upstream halo2 v0.4.0 with + 4 surgical patches (see `vendor/halo2/PATCHES.md`): + 1. `VerifyingKey::cs()` `pub(crate)` -> `pub` + 2. `VerifyingKey::permutation()` accessor (new) + 3. `permutation::VerifyingKey` + `commitments()` accessor (made `pub`) + 4. `ParamsKZG::{g, g2, s_g2}` accessors (new) +* The vendored workspace compiles cleanly under `cargo check + --workspace` from `vendor/halo2/`. +* The project root `Cargo.toml` still pins halo2_proofs to v0.3 git tag + so Stages A and B remain green; flipping to the vendored path requires + porting every v0.3 -> v0.4 API call site (`Any::Advice` tuple variant, + `ConstraintSystemBack`, `Repr<32>` vs `[u8; 32]`, `compile_circuit`, + etc.) in `src/lowering.rs`, `src/transcript.rs`, `src/test.rs`, and + `examples/separately.rs`. That is a multi-day effort and is intentionally + not bundled in the same commit. + +## What works + +* `cargo check`, `cargo test --lib function_signature` pass cleanly. +* `templates/contracts/Halo2VerifyingKey.sol` and the prelude of + `templates/contracts/Halo2Verifier.sol` produce a 100% BLS12-381 / EIP-2537 layout: + G1 = 4 words, G2 = 8 words, EIP-2537 padding (16 zero bytes + 48 byte + value per Fp coord), BLS12-381 scalar field modulus, precompile calls to + `0x0b` / `0x0c` / `0x0f`. +* The Rust crate now exposes the `halo2curves::bls12381` types via the + `halo2curves = "0.7"` dependency (`halo2_proofs` v0.3 still uses 0.6 + internally, which is fine -- both versions co-exist in the dep tree). + +## Known cryptographic caveats + +### 1. The Rust prover backend is still BN254 + +The fundamental blocker is that **`halo2_proofs` v0.3 has no KZG backend +for BLS12-381**. The PSE main branch (`v0.4`) pulls `halo2curves 0.7` (which +*does* have BLS12-381) but the API has been split into multiple crates and +the porting work is much larger than this diff. Until a halo2 KZG-BLS +backend is wired in: + +* `SolidityGenerator::new` still takes `&ParamsKZG` and + `&VerifyingKey`. +* `src/lowering.rs::generate_vk` calls `bls_g1_pad_from_bn254_bytes` / + `bls_g2_pad_from_bn254_bytes` which **zero-extend the 32-byte BN254 + coordinates to 48 bytes** before splitting per EIP-2537. The resulting + bytes are *not* valid BLS12-381 curve points -- they're shape-correct + scaffolding so the rest of the codegen pipeline keeps working. +* The pairing precompile call will revert at runtime because the embedded + points aren't on the BLS curve. **This is expected** until we have a + real BLS prover. + +### 2. In-EVM Fp arithmetic in `pcs.rs` is broken + +The BN254 verifier did some Fp arithmetic inline in EVM (e.g. computing the +quotient commitment via `mulmod`, evaluating PCS opening polynomials in +`Fp`). For BLS12-381: + +* `Fp` is 381 bits, so `mulmod(_, _, p)` is not expressible in EVM (uint256 + cap). Any in-EVM Fp arithmetic must be replaced with EIP-2537 precompile + calls plus modular helpers expressed over (hi, lo) splits. +* `EcPoint` in `src/lowering/util.rs` still tracks only `x` and `y` as single + u256 words. We bumped its stride to 4 (so memory layout is correct), but + the actual Yul emitter in `pcs.rs` still treats + points as `(x, y)` of single u256 each. + +The pcs blocks therefore emit Yul that **won't compile cleanly** against the +new BLS12-381 layout. The `#[ignore]`'d render tests would surface this if +you re-enabled them. + +### 3. Transcript is still BN254-shape + +`src/transcript.rs` writes 32 bytes per Fp coord; for BLS12-381 it should +write 48 bytes (or 64 with EIP-2537 zero padding). The transcript module +needs new `read_point` / `write_point` paths. Same for `encode_calldata` if +we want strict typing -- `encode_calldata` currently treats `proof` as +opaque bytes which is fine, but the proof bytes a real BLS halo2 fork emits +will have a different layout from what the test fixtures here produce. + +### 4. Generated artifacts under `generated/` + +The committed `generated/Halo2Verifier.sol` is a stale BN254 artifact from +`main`. It is **not** regenerated against the BLS pipeline. Once the gaps +above are closed we should regenerate it. + +## What's still to do + +1. **Wire the vendored halo2 v0.4 in.** `vendor/halo2/` is the prepared + landing pad with 4 surgical patches applied (see + `vendor/halo2/PATCHES.md`). To activate it: + * Switch root `Cargo.toml` from + `halo2_proofs = { git = "...", tag = "v0.3.0" }` to + `halo2_proofs = { path = "vendor/halo2/halo2_proofs" }`. + * Find a v0.4-compatible replacement for the `halo2_maingate` dev-dep + (current pin v2024_01_31 is v0.3-shaped). Either bump to a newer + maingate tag, or replace the `MainGate` test circuit with a custom + one that doesn't depend on maingate. + * Update every v0.3 -> v0.4 API call site: + - `Any::Advice` is no longer a tuple variant -- destructuring patterns + in `src/lowering.rs::ConstraintSystemMeta::new` need to be rewritten. + - `ConstraintSystem` -> `ConstraintSystemBack` for downstream codegen + reads. + - `Repr<32>` vs `[u8; 32]` differences in `src/transcript.rs`. + - `compile_circuit` is required between v0.4 frontend and backend. + - `ProverSHPLONK` / `VerifierSHPLONK` / `ParamsKZG` API tweaks. +2. **Switch SolidityGenerator types from BN254 to BLS12-381.** Once v0.4 + compiles, the only remaining cryptographic gap is replacing + `&ParamsKZG` / `&VerifyingKey` with + `&ParamsKZG` / `&VerifyingKey` + throughout `src/lowering.rs` and the prover side in `examples/separately.rs`. + The codegen helpers `g1_to_u256s` and `g2_to_u256s` already speak the + correct EIP-2537 layout for real BLS points. +3. **Update `src/transcript.rs`.** `Keccak256Transcript` should read 96 + bytes per G1 commitment (48 + 48 BE) and append them to the running hash. + Scalar (Fr) reads stay at 32 bytes. +4. **Refresh `generated/`.** Regenerate the canonical Solidity outputs once + the BLS prover swap is done, and add a deterministic-render snapshot test. +5. **Reactivate `#[ignore]` tests.** As each layer is ported the + corresponding render / pbt tests should be re-enabled. `function_signature`, + the BLS codegen smoke tests, and + `prague_evm_runs_eip2537_g1add_to_identity` already pass; `render_*` + require a working BLS prover; `pbt_*` require `cargo test --release`. + +## Useful pointers + +* EIP-2537 spec: https://eips.ethereum.org/EIPS/eip-2537 +* `halo2curves` BLS12-381 module: + `~/.cargo/registry/src/.../halo2curves-0.7.0/src/bls12381/` +* PSE halo2 main (v0.4 with halo2curves 0.7): + https://github.com/privacy-scaling-explorations/halo2 + +## Quick health check + +``` +cargo check --lib # Should build clean +cargo check --tests --examples --features evm # Should build clean (warnings ok) +cargo test --lib # 3 pass, 17 ignored +cargo run --example separately --features evm # Deploys + calls verifier per k + +# Verify the vendored halo2 v0.4 still compiles on its own: +( cd vendor/halo2 && cargo check --workspace ) +``` + +If any of those fail, something has regressed in the scaffolding layer; fix +that before chasing pcs / transcript work. diff --git a/proofs/solidity-verifier/docs/reference/PR17_BORROWED_IDEAS.md b/proofs/solidity-verifier/docs/reference/PR17_BORROWED_IDEAS.md new file mode 100644 index 000000000..e9ab358d8 --- /dev/null +++ b/proofs/solidity-verifier/docs/reference/PR17_BORROWED_IDEAS.md @@ -0,0 +1,72 @@ +# Borrowed Ideas From privacy-ethereum PR #17 + +This repo borrows a narrow set of engineering ideas from +`privacy-ethereum/halo2-solidity-verifier` PR #17 without adopting its full +runtime-reusable verifier architecture. + +## What We Borrow + +- **Typed artifact payload layout.** Generated VK bytecode is treated as named + sections: header, quotient constants, quotient program, fixed commitments, + and permutation commitments. `VkPayloadLayout` validates that these sections + are monotonic, non-overlapping, and byte-length consistent with the rendered + `Halo2VerifyingKey.sol` runtime. +- **Explicit packed-program codec.** Quotient VM bytecode is packed into + 32-byte VK words through `PackedProgramCodec`, with tests for explicit byte + lengths, out-of-capacity lengths, and non-zero padding. +- **Artifact mutation tests.** The Solidity regression suite now mutates each + correctness-critical VK payload section and checks that the pinned verifier + rejects the changed artifact. + +## What We Do Not Borrow + +- No `Halo2VerifierReusable` contract. +- No dynamic verifier configuration loaded from an untrusted runtime artifact. +- No BN254/PSE Halo2 assumptions. +- No selector-compression, mvlookup, or logup code from that PR. +- No replacement of this repo's Askama templates, external pinned quotient + evaluator, compact quotient VM/native callbacks, fused PCS MSM, Keccak + transcript, or BLS12-381/EIP-2537 backend. + +## Why Static And Pinned + +This verifier generator targets Midfall/Midnight proof shapes and treats the +Rust verifier plus Rust/Solidity trace equivalence as the source of truth. The +VK and quotient evaluator are deployed as separate contracts for code-size +reasons, but both are pinned by expected runtime length and codehash before +verification. + +That static shape is intentional: + +- the verifier bytecode still commits to one concrete protocol plan; +- external artifacts cannot silently swap computation tables; +- trace equivalence continues to compare deterministic Solidity checkpoints + against the real Rust verifier; +- gas remains driven by the current optimized verifier rather than a broad + dynamic interpreter. + +The borrowed PR #17 ideas therefore live only at the generator-validation layer: +they make payload offsets, packed bytecode, and mutation coverage more explicit +without changing the verifier's runtime trust model. + +## Regression Hooks + +Useful checks after touching artifact layout or quotient program packing: + +```bash +cargo test --lib \ + --features evm,rust-verifier-trace,truncated-challenges,in-circuit-fewer-point-sets \ + -- --nocapture +``` + +```bash +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ +HALO2_SOLIDITY_RUN_EVM_TESTS=1 \ +cargo test --lib \ + --features evm,rust-verifier-trace,truncated-challenges,in-circuit-fewer-point-sets \ + vk_payload_section_mutations_are_rejected \ + -- --nocapture +``` + +Then run the full IVC trace-equivalence and detailed bench commands from +`README.md` before treating the change as production-relevant. diff --git a/proofs/solidity-verifier/docs/reference/QUOTIENT_EVALUATOR_9KB_BYTECODE.md b/proofs/solidity-verifier/docs/reference/QUOTIENT_EVALUATOR_9KB_BYTECODE.md new file mode 100644 index 000000000..63e06a6b2 --- /dev/null +++ b/proofs/solidity-verifier/docs/reference/QUOTIENT_EVALUATOR_9KB_BYTECODE.md @@ -0,0 +1,294 @@ +# How the Halo2 Quotient Evaluator Reached 9 KB + +This note explains the engineering path that took the split +`Halo2QuotientEvaluator` down to a `9646` byte deployed runtime in the IVC +Keccak bench. + +The number is for the split-evaluator shape used by the IVC bench, where the +quotient numerator body lives in `Halo2QuotientEvaluator` and the main verifier +pins that helper by runtime length and codehash. + +## Benchmark Shape + +The `9646` byte result was measured with: + +```text +solc: 0.8.30 +optimizer runs: 1 +CBOR metadata: omitted +outer single-H commitment: disabled in the local bench script +quotient representation: compact VM plus selected native callbacks +``` + +Measured split artifacts: + +```text +Halo2Verifier runtime: 12,061 bytes +Halo2VerifyingKey runtime: 17,024 bytes +Halo2QuotientEvaluator runtime: 9,646 bytes +total runtime: 38,731 bytes + +checkpoint 12: 328,105 gas +total tx gas: 1,315,829 gas +``` + +The later merged-verifier run removed the separate evaluator and measured: + +```text +Halo2Verifier runtime: 21,374 bytes +Halo2VerifyingKey runtime: 17,024 bytes +total runtime: 38,398 bytes + +checkpoint 12: 312,018 gas +total tx gas: 1,296,262 gas +``` + +So the 9 KB evaluator was not the final deployment goal by itself. It was the +bytecode reduction that made the quotient body small enough to merge back into +the verifier. + +## The Short Version + +The evaluator became small because the generator stopped emitting every quotient +identity as straight-line Yul bytecode. Instead it moved most identity arithmetic +into a compact program stored in the verifying-key payload, and kept only a +small interpreter plus carefully chosen native callbacks in evaluator bytecode. + +That changed the split from: + +```text +one generated Yul block per identity +``` + +to: + +```text +fixed interpreter bytecode ++ VK-resident q_program bytes ++ VK-resident constant table ++ native callbacks only when the byte/gas trade is worth it +``` + +This is a code-size trade: some information moved out of evaluator runtime code +and into VK data. The verifier still checks the same quotient numerator +relationship. + +## Step 1: Split the Quotient Numerator Body + +The original reason for `Halo2QuotientEvaluator` was EIP-170 pressure. The +quotient numerator reconstruction is the largest scalar arithmetic section of +the generated verifier. + +In split mode, the main verifier: + +1. Loads the VK payload. +2. Samples transcript challenges. +3. Decodes proof evaluations. +4. Computes shared values such as `x^n`, Lagrange values, and public input + evaluation. +5. Calls the quotient evaluator with a raw memory frame. + +The evaluator receives the already prepared frame and only reconstructs: + +```text +linearization expected scalar = -nu_y(x) +simple selector accumulator buckets +``` + +This split did not make the quotient arithmetic itself cheap, but it gave the +quotient body a separate EIP-170 budget and a much smaller interface than a +normal ABI call. + +## Step 2: Move Identities Into a Compact VM + +The largest win was the compact quotient VM. + +Instead of rendering most identities as Yul source, the generator lowers them to +a small bytecode language in `src/lowering/quotient/mod.rs`. The runtime consumer +is `templates/partials/quotient_numerator/QuotientNumeratorBlock.yul`. + +The VK payload carries: + +- a deduplicated Fr constant table; +- an encoded quotient program; +- offsets to both tables. + +The evaluator bytecode carries: + +- the interpreter loop; +- only the opcode switch arms used by the generated program; +- native callbacks selected by the generator. + +For the measured IVC shape, the profile showed a compact program of roughly +`4931` bytes and `189` constants. Those bytes live in the VK payload, not as +unrolled evaluator runtime code. + +## Step 3: Make the VM Dense + +A naive stack VM would still be too large and too slow. The quotient VM uses +several density tricks: + +- memory tokens for common verifier slots such as `beta`, `gamma`, `x`, + `theta`, `L_0`, and `L_last`; +- short encodings for small constants and offsets; +- packed instruction forms where possible; +- run opcodes for repeated add/mul shapes; +- domain-specific limb opcodes for repeated 7-limb foreign-field arithmetic; +- optional opcode switch arms, so unused interpreter cases do not get rendered. + +The important idea is that the VM is not a general EVM-in-EVM. It is a small +quotient-expression machine tailored to the shapes this generator emits. + +## Step 4: Keep Native Code Only Where It Pays + +Some identities are bad VM candidates. For those, native Yul is still cheaper at +runtime, but native Yul costs bytecode. The generator therefore keeps a bounded +native-callback budget. + +The first heuristic selected the heaviest VM identities by byte length. That was +too crude: a large VM identity is not always a good bytecode purchase. + +The current selector builds candidates with: + +```text +VM byte estimate +VM gas estimate +native Yul byte estimate +native Yul gas proxy +``` + +Then it solves a small byte-budget knapsack. In the bytecode-lean `9646` byte +profile, the score is conservative: + +```text +estimated saving = VM gas - discounted native Yul gas +``` + +That kept the small profitable native callbacks and avoided spending runtime +bytecode on large callbacks that were not worth their deployed-size cost. + +The default count is fixed at `4`, and the selected four are chosen by estimated +gas saved under the byte budget rather than by top-N VM byte length. + +## Step 5: Keep Structured Family Callbacks + +Permutation and lookup identities have regular product-loop structure. Encoding +those loops entirely as generic VM opcodes would save bytecode but cost too much +gas. + +The generator keeps structured native callbacks for these families in the +default IVC shape. + +These callbacks reuse the quotient VM stack base as scratch, so the Rust memory +planner reserves enough words for the larger of: + +- interpreted operand stack needs; +- native callback scratch-table needs. + +This was necessary to make the compact path safe without overallocating memory +in unrelated verifier sections. + +## Step 6: Remove the Selector Inverse Fold + +Simple selector identities are known at codegen time. Earlier versions still +computed `y^-1` at runtime and maintained selector scale/inverse-scale state +while walking identities. + +The replacement is a gap-based selector fold: + +1. Codegen records the distance between selector identity positions. +2. Runtime precomputes only the needed `y^k` powers. +3. A selector bucket is scaled only when that selector appears. +4. A final tail gap is applied after the VM scan. + +This removed one runtime modexp and repeated per-identity selector maintenance. +In the measured path it was mainly a deployed-size win, and it simplified the +state that the VM has to maintain. + +## Step 7: Gate Production Trace Hooks + +Trace-equivalence builds need quotient identity trace events. Production builds +do not. + +The quotient numerator block previously carried trace-id state and called the +trace hook even in production. In the split evaluator the logless trace hook +still did memory work, and the surrounding shape affected generated bytecode. + +The production path now gates trace emission behind trace builds. That saves +both gas and bytecode in the quotient section while preserving trace builds for +differential testing. + +## Step 8: Include Helpers Only When Used + +`QuotientHelpers.yul` is shared by monolithic and split paths, but the generator +tracks which helper shapes are actually referenced: + +- `q_pow5` +- `q_limb7` +- `q_limb7_wide` + +Unused helpers are not emitted into the quotient runtime. This matters because +helper bodies are small individually, but the quotient evaluator was being +optimized under a tight bytecode budget. + +## What Did Not Change + +The 9 KB result did not come from weakening the verifier. + +The generated Solidity still: + +- reconstructs the same y-batched quotient numerator `nu_y(x)`; +- stores `-nu_y(x)` as the expected linearization scalar; +- accumulates simple selector buckets for the final linearization commitment; +- checks the proof through the same PCS and pairing flow; +- uses the same transcript challenge order. + +The optimization is representational: arithmetic moved from unrolled runtime +Yul into compact VK-resident data plus a small interpreter. + +## Why 9 KB Was Enough to Merge Back + +After the quotient body reached `9646` bytes as a split evaluator, the merged +verifier could fit under EIP-170: + +```text +merged Halo2Verifier runtime: 21,374 bytes +EIP-170 limit: 24,576 bytes +headroom: 3,202 bytes +``` + +Merging removes: + +- the quotient evaluator deployment; +- the verifier constructor's quotient address argument; +- quotient runtime length/codehash pinning; +- the `STATICCALL`; +- frame copy and output-frame validation. + +That is why the merged benchmark improved checkpoint 12 from `328,105` to +`312,018` gas and total transaction gas from `1,315,829` to `1,296,262`. + +## Reproduction Notes + +The exact `9646` byte standalone evaluator number belongs to an earlier split +path. The commit that recorded that shape was: + +```text +226c124 Select native quotient gates by gas-byte knapsack +``` + +The command used was: + +```sh +scripts/run_ivc_bench.sh --skip-srs-download +``` + +with the local bench script setting: + +```text +OUTER_SINGLE_H_COMMITMENT=0 +``` + +The current IVC bench writes `Halo2QuotientEvaluator.sol` alongside +`Halo2Verifier.sol`; inspect `target/ivc-keccak-solidity-dump/` after a bench +run to review the standalone evaluator. diff --git a/proofs/solidity-verifier/docs/reference/QUOTIENT_NUMERATOR_EVALUATOR.md b/proofs/solidity-verifier/docs/reference/QUOTIENT_NUMERATOR_EVALUATOR.md new file mode 100644 index 000000000..94bc90d7f --- /dev/null +++ b/proofs/solidity-verifier/docs/reference/QUOTIENT_NUMERATOR_EVALUATOR.md @@ -0,0 +1,1068 @@ +# Quotient Numerator Evaluator + +This document explains the generated `Halo2QuotientEvaluator` contract: what +data it receives, what it computes, how the result is consumed by +`Halo2Verifier`, and how every step maps back to the Midfall Rust verifier. + +The important mental model is: + +```text +Rust verifier: + partially_evaluate_identities(...) + compute_linearization_commitment(...) + +Solidity split verifier: + Halo2Verifier prepares the same transcript challenges and evaluations + Halo2QuotientEvaluator reconstructs the same batched numerator scalar data + Halo2Verifier folds those scalar data into the final PCS check +``` + +The Solidity evaluator is not a second verifier design. It is a lowered, +size-aware implementation of the same Rust verifier steps. + +The complete upstream comment corpus is preserved in +`docs/reference/MIDFALL_PROOFS_COMMENT_CORPUS.md`. This document adapts only the comments +that directly explain the generated quotient path: identity ordering, simple +selector handling, LogUp/permutation/trash evaluations, and the linearization +commitment sign convention. + +## Contract Role + +The quotient numerator block can be rendered directly inside `Halo2Verifier` or +as the external `Halo2QuotientEvaluator` helper used by the split verifier +mode. The block exists because the batched identity numerator is the largest +generated arithmetic section. Keeping it internal avoids a `STATICCALL` and +memory-frame copy when the merged verifier remains below EIP-170; splitting it +keeps both the main verifier and evaluator below the runtime-code limit for +larger circuits. + +In split mode, the evaluator: + +1. receives a raw memory frame from `Halo2Verifier`; +2. copies that frame into the same generated memory addresses; +3. runs the generated batched identity numerator reconstruction; +4. returns a compact output frame containing the linearization expected scalar + and simple-selector accumulator scalars. + +The evaluator does not: + +- parse the public ABI; +- read proof calldata directly; +- perform transcript hashing; +- evaluate a prover-supplied quotient scalar; +- build or check elliptic-curve commitments. + +Those jobs stay in `Halo2Verifier`. + +## Trust Boundary + +The quotient evaluator is correctness-critical. A malicious evaluator could +return a different expected scalar or different selector accumulators, changing +the statement the verifier checks. For that reason the main verifier pins the +quotient evaluator by generated runtime length and codehash: + +```solidity +EXPECTED_QUOTIENT_LENGTH +EXPECTED_QUOTIENT_CODEHASH +``` + +The verifier checks those constants at construction. The evaluator output also +carries a generated magic word, and the verifier rejects outputs with the wrong +size or wrong magic. + +## Frame ABI + +`Halo2QuotientEvaluator` has only a fallback entry point. Its calldata is not +normal Solidity ABI. It is exactly the verifier memory image: + +```text +calldata[0..QUOTIENT_FRAME_LEN) == memory[QUOTIENT_FRAME_BASE..+QUOTIENT_FRAME_LEN) +``` + +The fallback rejects any calldata length other than `QUOTIENT_FRAME_LEN`: + +```yul +if iszero(eq(calldatasize(), QUOTIENT_FRAME_LEN)) { revert(0, 0) } +calldatacopy(QUOTIENT_FRAME_BASE, 0, QUOTIENT_FRAME_LEN) +``` + +The copied frame contains: + +- verifier-key words, including domain constants and the quotient VM payload; +- Fiat-Shamir challenges already sampled by `Halo2Verifier`; +- proof evaluations already decoded and range-checked by `Halo2Verifier`; +- Lagrange values `l_last`, `l_blind`, and `l_0`; +- the locally computed public-instance evaluation; +- scratch/output space for selector accumulators. + +The output frame is compact: + +```text +word 0: QUOTIENT_MAGIC +word 1: linearization_expected_eval +word 2..: simple-selector accumulator scalars, one word each +``` + +`Halo2Verifier` writes `word 1` to `QUOTIENT_EVAL_MPTR` and copies the +selector words back to `SELECTOR_ACC_MPTR`. + +## Rust Source Of Truth + +The relevant Rust files are: + +- `midfall/proofs/src/plonk/verifier.rs` +- `midfall/proofs/src/plonk/mod.rs` +- `midfall/proofs/src/plonk/linearization/verifier.rs` +- `midfall/proofs/src/plonk/permutation.rs` +- `midfall/proofs/src/plonk/logup.rs` +- `midfall/proofs/src/plonk/trash.rs` + +When this repository copies or adapts those comments into Solidity/Yul, it keeps +the upstream role intact: the comments explain the Rust verifier behavior first, +then name the local memory slot or generated block that ports it. + +The Rust verifier flow around the quotient numerator is: + +1. read quotient commitment(s) from the transcript; +2. sample the evaluation challenge `x`; +3. compute `splitting_factor = x^(n - 1)` and `x^n`; +4. read or compute all evaluations used by identities; +5. call `partially_evaluate_identities`; +6. call `compute_linearization_commitment`; +7. add the linearization query to the PCS multi-open check. + +The generated Solidity follows the same order. The external evaluator begins +only after `Halo2Verifier` has completed steps 1 through 4. + +## Quotient Commitments Are Not Quotient Scalars + +The Rust comment in `verifier.rs` says the verifier reads commitments to: + +```text +h(X) = nu(X) / (X^n - 1) +``` + +In the multi-limb case, the prover commits to limbs of `h`. Those are G1 +commitments, not scalar evaluations trusted from the proof. + +The Solidity evaluator reconstructs the numerator side: + +```text +nu_y(x) +``` + +and stores: + +```text +linearization_expected_eval = -nu_y(x) +``` + +It does not compute: + +```text +h(x) = nu_y(x) / (x^n - 1) +``` + +The commitment side supplies the quotient factor separately: + +```text +(1 - x^n) * sum_i splitting_factor^i * Q_i +``` + +Since `(1 - x^n) = -(x^n - 1)`, this is the same sign convention as the Rust +linearization formula, just arranged so the scalar side is `-nu_y(x)`. + +## Evaluation Inputs + +After `x` is sampled, Rust reads or computes the evaluations that feed +`partially_evaluate_identities`. + +### Instance Evaluations + +For committed instance columns, Rust reads the evaluation from the transcript. +For public instance columns, Rust computes the evaluation locally by Lagrange +interpolation against the public inputs. + +Solidity mirrors this split: committed instance evaluations are proof scalars, +while the non-committed public input evaluation is computed in the Lagrange +block and stored in the quotient frame. + +### Advice Evaluations + +Rust reads one evaluation for each advice query and each proof. Solidity reads +the same scalars into generated memory slots after transcript challenge `x`. + +### Fixed Evaluations And Simple Selectors + +Rust reads: + +```text +num_fixed_columns - num_simple_selectors +``` + +fixed evaluations from the transcript. Then it inserts `F::ONE` into the +missing positions for simple selector columns. The Rust comment explains that +the proof does not contain evaluations for multiplicative simple selectors. + +Solidity uses the same convention. Simple selector identities are not fully +evaluated into the scalar numerator. Instead, they are accumulated into +selector buckets and later paired with fixed selector commitments in the +linearization MSM. + +### Permutation, Lookup, And Trash Evaluations + +Rust obtains these through their argument-specific `evaluate` methods. Solidity +has already decoded their corresponding proof scalars before the external +quotient call: + +- permutation common/sigma evaluations and product polynomial evaluations; +- lookup multiplicity, helper, accumulator, and next-accumulator evaluations; +- trash evaluation scalars. + +## Identity List Shape + +`mod.rs::partially_evaluate_identities` returns: + +```text +Vec<(Option, F)> +``` + +The `Option` is the linearization target: + +- `Some(selector_column_index)` means the identity is gated by a simple fixed + selector and should become a selector commitment scalar; +- `None` means the identity is fully evaluated and contributes only to the + expected opening scalar. + +The Rust iterator order is: + +1. custom gate identities; +2. permutation identities; +3. lookup identities; +4. trash identities. + +The generated quotient program preserves this order exactly. + +## Gate Identities + +For each custom gate polynomial, Rust evaluates the expression with: + +- constants mapped directly; +- fixed/advice/instance queries mapped to their evaluations at `x`; +- challenge queries mapped to sampled challenges; +- expression nodes lowered through negation, addition, multiplication, and + scalar multiplication. + +Virtual selectors are expected to have been removed during optimization. + +For each gate, Rust finds the first queried simple selector, if any, and +returns that selector column index alongside the evaluated identity. Solidity +does the same classification during code generation. + +## Permutation Identities + +The Rust comments in `permutation.rs` describe four kinds of constraints. +Solidity emits the same sequence. + +### First Set Boundary + +Only the first set enforces: + +```text +l_0(x) * (1 - z_0(x)) +``` + +### Last Set Boolean Boundary + +Only the last set enforces: + +```text +l_last(x) * (z_l(x)^2 - z_l(x)) +``` + +### Cross-Set Continuity + +For every set after the first, Rust enforces: + +```text +l_0(x) * (z_i(x) - z_{i-1}(omega^last * x)) +``` + +The verifier reads the previous set's last-row product evaluation for this. + +### Active-Row Product Check + +For every set, Rust enforces on active rows: + +```text +(1 - (l_last(x) + l_blind(x))) * +( + z_i(omega*x) * product(p(x) + beta*s_i(x) + gamma) + - z_i(x) * product(p(x) + delta^i*beta*x + gamma) +) +``` + +The generated Solidity keeps this product looped over permutation chunks. It +uses the same `delta`, `beta`, `gamma`, `x`, `l_last`, and `l_blind` values. + +## Lookup Identities + +`logup.rs::Evaluated::expressions` documents the LogUp checks. When a lookup +has multiple columns, Rust compresses expressions with `theta`: + +```text +acc = acc * theta + eval +``` + +Both input values `f_j` and table value `t` are compressed this way. + +The emitted identities are: + +### Boundary + +```text +(l_0(x) + l_last(x)) * Z(x) +``` + +### Helper Constraints + +For each lookup input chunk: + +```text +h_i(x) * product_j(f_j(x) + beta) + - sum_j product_{k != j}(f_k(x) + beta) +``` + +### Accumulator Constraint + +On active rows: + +```text +( + Z(omega*x) + - Z(x) + - selector(x) * sum_i h_i(x) +) * (t(x) + beta) ++ m(x) +``` + +then multiplied by: + +```text +1 - (l_last(x) + l_blind(x)) +``` + +The Rust comment notes that the selector gates only the input-side helper sum. +Multiplicities are always subtracted so table-side balance is maintained. +Solidity follows that same convention. + +## Trash Identities + +`trash.rs::Evaluated::expressions` compresses each trash argument's constraint +expressions with `trash_challenge` using Horner form: + +```text +compressed = (((expr_0) * trash_challenge + expr_1) ...) +``` + +Then it subtracts the inactive-row trash term: + +```text +compressed - (1 - q(x)) * trash_eval +``` + +The Rust `required_degree` comment notes that the degree is at least two +because of the `(1 - q) * trash` product. Solidity emits the same formula in +the structured trash suffix used by the default IVC verifier. + +## Y-Batching Algebra + +Rust linearization groups identity evaluations in reverse: + +```text +y_pow = 1 +for (selector, eval) in expressions.rev(): + grouped[selector] += y_pow * eval + y_pow *= y +``` + +If the forward identity order is: + +```text +e_0, e_1, ..., e_{m-1} +``` + +then the Rust scalar for `e_i` is: + +```text +y^(m - 1 - i) * e_i +``` + +Solidity scans forward with Horner form: + +```text +acc = 0 +for e_i in forward_order: + acc = acc * y + e_i +``` + +After all identities, this is: + +```text +y^(m-1)*e_0 + y^(m-2)*e_1 + ... + e_{m-1} +``` + +which matches Rust. + +## Selector Accumulator Algebra + +Simple selector identities must keep the same y-batch position but cannot be +added to the fully evaluated numerator. Rust groups them by selector commitment +in a `BTreeMap, F>`. + +The compact VM precomputes the `y^k` powers needed by selector gaps and tails. +For each selector bucket it tracks only the last emitted selector position in +the generated bytecode schedule. + +```text +quotient_eval_numer +selector_bucket[j] +y_power[k] = y^k +``` + +For a selector identity at global identity index `i`, codegen emits the gap +from the previous identity using the same selector: + +```text +selector_bucket[j] = selector_bucket[j] * y_power[gap] + eval +``` + +After the full identity stream, codegen applies each selector's final tail: + +```text +selector_bucket[j] *= y_power[m - 1 - last_index[j]] +``` + +An evaluation at index `i` therefore receives: + +```text +eval * y^(m - 1 - i) +``` + +This is the same power assigned by the Rust reverse fold, without computing +`y^-1` or updating selector scale state for unrelated identities. + +## Linearization Output + +`linearization/verifier.rs::compute_linearization_commitment` builds one +linear query at `x`. + +The Rust comment describes the linearized commitment as: + +```text +S_0 * id_0(x) + y*S_1*id_1(x) + ... + - (h_0 + x^(n-1)*h_1 + ... ) * (x^n - 1) +``` + +where each `S_j` is either: + +- a fixed commitment for a simple selector; or +- the constant polynomial `1` for fully evaluated identities. + +In implementation terms: + +- quotient commitments are added with powers of `splitting_factor`; in the + outer single-H layout there is one such term, with scalar `1 - x^n`; +- simple selector buckets become fixed commitment scalars; +- `None` identities are subtracted into `expected_eval`. + +Solidity splits this across two contracts: + +- `Halo2QuotientEvaluator` returns `expected_eval` and selector buckets; +- `Halo2Verifier` expands quotient commitment(s) and selector commitments into + the fused PCS final MSM. + +## Outer Single-H Effect + +Midfall's `single-h-commitment` feature changes how the prover commits to the +quotient polynomial `h(X)`. Instead of committing to `cs.degree() - 1` degree +bounded limbs, the prover commits once to the full `h(X)`. This repository +exposes that as `outer-single-h-commitment` and forwards it only to +`midnight-proofs`, so only the final Solidity-facing decider proof changes. +The recursive leaf proofs verified inside the decider circuit keep their +multi-limb proof layout. + +This is a commitment-side change, not a numerator-side change. The evaluator +still reconstructs the same ordered identity stream, consumes the same +evaluation scalars, computes the same selector buckets, and returns the same +`linearization_expected_eval = -nu_y(x)`. Trace-compatible builds still keep +the same linearization scalar trace shape, including `x_split = x^(n - 1)` and +`1 - x^n`. + +The visible verifier changes are downstream of the evaluator: + +- transcript/proof layout reads one quotient commitment instead of + `cs.degree() - 1`; +- PCS block 5 receives fewer quotient commitment terms in the fused final MSM; +- for the current degree-5 IVC decider this shrinks that MSM from `78` to `75` + terms; +- the batched identity numerator reconstruction checkpoint is expected to stay + essentially unchanged. + +In the current profiled IVC command, that shows up as `533,202 -> 515,290` gas +in PCS block 5 and `1,399,268 -> 1,374,697` total transaction gas. The +numerator checkpoint remains about `412k` gas in both profiles because it is +still evaluating the same identities. + +## Generated Evaluator Structure + +The generated evaluator has four execution regions. + +### Direct Inline Prefix + +The first few gate identities are emitted directly. This is a small, +well-tested anchor and avoids routing every identity through the VM. + +### Compact `q_program` VM + +Most identity arithmetic is stored as a compact bytecode program in the VK +payload. The evaluator reads constants and program words from the copied frame +and interprets them with a small stack machine. + +The VM has opcodes for: + +- pushing constants; +- loading memory-backed evaluations/challenges; +- field add/mul/neg; +- fused add/mul forms used by expression lowering; +- optional limb-aware non-SHA foreign-field shapes; +- folding main identities; +- folding selector identities; +- invoking native callbacks. + +The VM is the bytecode-size lever: moving identities into it usually shrinks +deployed bytecode but costs more runtime gas. + +The bytecode is part of the generated, pinned VK payload, not a runtime input +chosen by the prover. Recent gas-capped profiles also specialize the Yul +interpreter to that finalized payload: codegen records which opcodes and memory +tokens appear after bytecode compaction/repacking and omits unreachable switch +arms from the rendered evaluator. This saves deployed runtime bytes and does +not make the verifier less correct in the current pinned-artifact model, but it +does mean the interpreter is no longer a universal implementation of every VM +opcode for arbitrary future VK payloads. + +For the broader distinction between a VK-specialized artifact and a truly +runtime-generic verifier, see `docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md`. + +### VM Interpreter Case Reference + +The source of truth for opcode numbers is `src/lowering/quotient_numerator/vm/mod.rs`; the +runtime interpreter is the `switch q_op` block in +`templates/partials/quotient_numerator/QuotientNumeratorBlock.yul`. The bytecode is generated into the VK +payload, not supplied by proof calldata. + +The interpreter is a small stack machine: + +- `q_top` caches the top stack value. +- `q_has_top` says whether `q_top` is live. +- `q_sp` points just past the spilled stack values in memory. +- Push operations spill the old `q_top` to `mstore(q_sp, q_top)` and advance + `q_sp` when `q_has_top` is set. +- Binary `ADD` and `MUL` pop one spilled value by decrementing `q_sp`, then + combine that value with `q_top`. +- Accumulator-style opcodes mutate `q_top` without changing stack depth. +- `FOLD_*` opcodes consume `q_top` and advance the y-batched identity stream. +- Native callbacks require the generated stream to be at an identity boundary, + reset `q_top`/`q_sp`, and perform their own fold operations. + +There is one physical encoding: `bytes`, a compact stream with one opcode byte +followed by variable-width big-endian operands. + +The logical cases are: + +| Opcode | Name | Byte operands | Interpreter effect | +|---|---|---|---| +| `0x01` | `push_const` | `u16 const_idx` | Push `mload(q_const_mptr + 32 * const_idx)`. | +| `0x02` | `push_mem_literal` | `u32 ptr` | Push `mload(ptr)`. | +| `0x03` | `push_mem_token` | `u8 token` | Resolve a symbolic memory token and push `mload(ptr_for_token(token))`. Unknown tokens revert. | +| `0x04` | `push_mem_token_offset` | `u8 token, u32 offset` | Resolve a token pointer, add the byte offset, and push `mload(ptr + offset)`. | +| `0x05` | `push_mem_u16` | `u16 ptr` | Short form of `push_mem_literal`: push `mload(ptr)`. | +| `0x06` | `add` | none | Pop one spilled operand and set `q_top = popped + q_top mod r`. | +| `0x07` | `mul` | none | Pop one spilled operand and set `q_top = popped * q_top mod r`. | +| `0x08` | `neg` | none | Set `q_top = -q_top mod r`. | +| `0x09` | `push_const_u8` | `u8 const_idx` | Short form of `push_const`. | +| `0x0a` | `fold_main` | none | Trace `q_top`, clear the top slot, multiply `quotient_eval_numer` by `y`, and add the evaluation into `quotient_eval_numer`. | +| `0x0b` | `fold_selector` | `u8 selector_idx, u16 selector_gap` | Trace `q_top`, clear the top slot, advance the same global y position, multiply that selector bucket by `y^selector_gap`, and add `q_top`. | +| `0x0c` | `add_const_u8` | `u8 const_idx` | Set `q_top += const[const_idx]`. | +| `0x0d` | `mul_const_u8` | `u8 const_idx` | Set `q_top *= const[const_idx]`. | +| `0x0e` | `add_const` | `u16 const_idx` | Wider form of `add_const_u8`. | +| `0x0f` | `mul_const` | `u16 const_idx` | Wider form of `mul_const_u8`. | +| `0x10` | `add_mem_u16` | `u16 ptr` | Set `q_top += mload(ptr)`. | +| `0x11` | `mul_mem_u16` | `u16 ptr` | Set `q_top *= mload(ptr)`. | +| `0x12` | `add_mul_mem_mem_const_u8` | `u16 lhs, u16 rhs, u8 const_idx` | Set `q_top += mload(lhs) * mload(rhs) * const[const_idx]`. | +| `0x13` | `add_mul_const_u8_mem_u16` | `u16 ptr, u8 const_idx` | Set `q_top += mload(ptr) * const[const_idx]`. | +| `0x14` | `add_mul_mem_mem` | `u16 lhs, u16 rhs` | Set `q_top += mload(lhs) * mload(rhs)`. | +| `0x15` | `run_add_mul_mem_mem_const_u8` | `u16 count`, then `count` copies of `u16 lhs, u16 rhs, u8 const_idx` | Dynamic run form of repeated `0x12`; each item accumulates into `q_top`. | +| `0x16` | `run_add_mul_const_u8_mem_u16` | `u16 count`, then `count` copies of `u16 ptr, u8 const_idx` | Dynamic run form of repeated `0x13`; each item accumulates into `q_top`. | +| `0x17` | `push_temp` | `u16 temp_idx` | Push `mload(q_tmp_mptr + 32 * temp_idx)`. Reserved for legacy temp payloads; current codegen does not emit it. | +| `0x18` | `store_temp` | `u16 temp_idx` | Store `q_top` to `q_tmp_mptr + 32 * temp_idx`; `q_top` remains live. Reserved for legacy temp payloads; current codegen does not emit it. | +| `0x19` | `native_permutation` | none | Reset the VM top/stack pointer and run the generated permutation callback at this identity position. The callback folds all permutation identities itself. | +| `0x1a` | reserved | none | No interpreter case. The default branch reverts if this opcode appears. | +| `0x1b` | `native_identity` | `u16 native_idx` | Reset the VM top/stack pointer and dispatch to one generated heavy-gate callback. Invalid callback indexes revert. | +| `0x1c` | `lin7` | seven copies of `u8 const_idx, u16 ptr` | Push `sum_i const[const_idx_i] * mload(ptr_i)`. Used for recognized 7-limb linear combinations. | +| `0x1d` | `bilin7_row` | `u16 lhs`, then seven copies of `u8 const_idx, u16 rhs` | Push `mload(lhs) * sum_i const[const_idx_i] * mload(rhs_i)`. | +| `0x1e` | `bilin7_pairwise` | `u16 lhs_base, u16 rhs_base, 13 bytes const_idx[0..12]` | Push `sum_{i=0..6,j=0..6} const[const_idx[i+j]] * mload(lhs_base + 32*i) * mload(rhs_base + 32*j)`. | +| `0x1f` | `native_lookup` | none | Reset the VM top/stack pointer and run the generated LogUp lookup callback at this identity position. The callback folds boundary, helper, and accumulator identities itself. | +| `0x20` | `pow5` | none | Set `q_top = q_top^5`. | +| `0x21` | `modarith7` | flags, optional condition/constant, count header, and fused 7-limb blocks | Push one fused affine 7-limb identity value. | +| `0x22` | `affine_sum` | `u16 lin_count, u16 product_count`, then linear and product payloads | Mixed dynamic affine accumulator over memory and memory-products. | + +The memory token switch used by `push_mem_token` and +`push_mem_token_offset` currently recognizes: + +| Token | Pointer | +|---|---| +| `0x01` | `L_0_MPTR` | +| `0x02` | `L_LAST_MPTR` | +| `0x03` | `L_BLIND_MPTR` | +| `0x04` | `BETA_MPTR` | +| `0x05` | `GAMMA_MPTR` | +| `0x06` | `X_MPTR` | +| `0x07` | `THETA_MPTR` | +| `0x08` | `TRASH_CHALLENGE_MPTR` | +| `0x09` | `INSTANCE_EVAL_MPTR` | + +The dynamic run opcodes, limb-aware opcodes, native callbacks, and structured +trash suffix are all part of the default IVC lowering. The generator +validates the finalized byte stream before embedding it in the VK payload: it +rejects unknown opcodes, truncated operands, invalid memory tokens, stack +underflow, and non-empty identity boundaries. + +### Native Callbacks + +Large recognized identities can be emitted as native Yul callbacks. The +default gas-capped compact mode keeps four heavy gate identities native, plus +the native permutation and lookup loops. + +Current gas-capped compact defaults: + +```text +direct inline identities: 4 +native heavy gate callbacks: 4 +native permutation callback: on +native lookup callback: on +structured trash suffix: on +VM CSE: off +limb VM ops: on +``` + +On the two-leaf IVC Keccak bench with the outer non-fewer proof layout, enabling +limb VM ops recognized `14` `LIN7` shapes and `7` `BILIN7_ROW` shapes. The +batched numerator checkpoint moved from about `425,852` gas to `412,748` gas, +while the quotient evaluator stayed below EIP-170 at `23,221` runtime bytes. + +Native callbacks preserve identity order because the VM stream contains an +opcode at the exact identity position. The callback computes the evaluation +and then returns to the shared fold logic. + +The lookup callback is a whole-family superinstruction for the LogUp block. It +replaces the interpreted boundary, helper-chunk, and accumulator identities +with a generated structured loop that shares `f + beta`, prefix-product, and +suffix-product scratch tables. + +### Structured Trash Suffix + +The default emits trash identities as a structured Yul suffix. This is a +measured gas/size trade because trash is regular enough to avoid too much +bytecode while saving VM dispatch overhead. + +## Limb Helpers + +The generated source may include: + +```text +q_pow5 +q_limb7 +q_limb7_wide +LIN7 +BILIN7_ROW +BILIN7_PAIRWISE +``` + +These helpers are not protocol rules. They are code-size/gas helpers for +recurring Midfall arithmetic shapes. The expression lowering still decides +what values are computed; the helpers only replace repeated Yul chains with +shared local functions when the generator recognizes an exact pattern. + +Unrecognized expressions fall back to the compact VM or direct generated Yul. + +### Are These Limbs Emulating A Different Field? + +Yes, but with an important verifier-side caveat. + +The Midfall foreign-field chips represent values from an emulated modulus `m` +as limbs: + +```text +value = limb_0 + limb_1 * B + ... + limb_6 * B^6 +``` + +where `B = 2^LOG2_BASE`, and the powers are reduced modulo `m`. The relevant +Rust source is: + +```text +circuits/src/field/foreign/params.rs::base_powers +circuits/src/field/foreign/params.rs::double_base_powers +circuits/src/field/foreign/util.rs::sum_exprs +circuits/src/field/foreign/util.rs::pair_wise_prod +``` + +The circuit uses this representation to emulate arithmetic in a different +field or modulus inside the native proving field. The Solidity verifier does +not perform arithmetic in that foreign field directly. It evaluates the PLONK +identity polynomial over the native BLS12-381 scalar field `Fr`, exactly as the +Rust verifier does in: + +```text +proofs/src/plonk/mod.rs::partially_evaluate_identities +proofs/src/plonk/verifier.rs +proofs/src/plonk/linearization/verifier.rs::compute_linearization_commitment +``` + +So the limb helpers are a compact lowering of already-generated constraint +expressions. They do not introduce a new trust assumption or a new runtime +field; they encode the same Fr polynomial identities that enforce congruences +modulo the foreign modulus `m` inside the circuit. + +### Rust Gate Mapping + +The helper names are Solidity codegen names. The Rust verifier does not call +`q_pow5`, `q_limb7`, or `q_limb7_wide`. Instead, +`mod.rs::partially_evaluate_identities` walks `vk.cs.gates`, evaluates each +gate polynomial, and returns the resulting `(Option, F)` items. The +Solidity generator sees those same expression trees and replaces a few common +subexpressions with helpers. + +`q_pow5(x)` is the Poseidon S-box shape: + +```text +q_pow5(x) = x^5 mod Fr = x * (x^2)^2 mod Fr +``` + +Rust source: + +```text +circuits/src/hash/poseidon/poseidon_chip.rs::sbox +circuits/src/hash/poseidon/poseidon_chip.rs::full_round_gate +circuits/src/hash/poseidon/poseidon_chip.rs::partial_round_gate +circuits/src/hash/poseidon/round_skips.rs::RoundId::to_expression +``` + +Those gates use quintic Poseidon terms either directly or through skipped-round +linear combinations. When the lowered expression contains five identical +multiplicative factors, the generator emits `q_pow5(base)` instead of repeating +the multiplication chain at every site. The same helper may appear in the trash +suffix when trash identities compress expressions that originated from +Poseidon-like constraints. + +`q_limb7(x0, ..., x6)` is the compact form of a 7-limb foreign-field linear +combination using `FieldEmulationParams::base_powers()`: + +```text +q_limb7(x0..x6) + = x0 + + c1*x1 + + c2*x2 + + c3*x3 + + c4*x4 + + c5*x5 + + c6*x6 + mod Fr +``` + +Rust source: + +```text +circuits/src/field/foreign/params.rs::base_powers +circuits/src/field/foreign/gates/norm.rs::Foreign-field normalization +circuits/src/field/foreign/gates/mul.rs::Foreign-field multiplication +``` + +In normalization, this corresponds to terms such as: + +```text +sum_exprs(base_powers, shifted_x) - sum_exprs(base_powers, zs) +``` + +In multiplication, it corresponds to the `sum_x`, `sum_y`, and `sum_z` limb +packing terms: + +```text +sum_exprs(base_powers, xs) +sum_exprs(base_powers, ys) +sum_exprs(base_powers, zs) +``` + +The constants in `q_limb7` are the generated Fr residues for this verifier's +7-limb foreign-field basis. They are not dynamic proof inputs. + +`LIN7` is the VM form of the same idea, but table-backed: + +```text +LIN7(values, coeffs) = sum_{i=0..6} coeff[i] * values[i] mod Fr +``` + +The generator emits it only after structurally recognizing a 7-term linear +combination over literal memory-backed evaluations. Coefficients are inserted +into the generated quotient constant table; proof calldata never selects +coefficient tables. + +`BILIN7_ROW` is the row form: + +```text +BILIN7_ROW(lhs, rhs[0..6], coeffs) + = sum_{i=0..6} coeff[i] * lhs * rhs[i] mod Fr +``` + +This is useful for foreign-field multiplication and EC formulas where one limb +is multiplied across a 7-limb vector. + +`q_limb7_wide(x0, ..., x6)` is the analogous helper for the product-convolution +side of the foreign-field multiplication gate. Rust computes all pairwise limb +products and weights them with `FieldEmulationParams::double_base_powers()`: + +```text +xys = pair_wise_prod(xs, ys) +sum_exprs(double_base_powers, xys) +``` + +The generated native gate code groups repeated 7-term slices of this wide basis +into calls to `q_limb7_wide`. This is why the native multiplication callbacks +contain many terms of the form `q_limb7_wide(a_i * b_0, ..., a_i * b_6)`. + +`BILIN7_PAIRWISE` is the VM form of the full 7-by-7 convolution: + +```text +BILIN7_PAIRWISE(lhs[0..6], rhs[0..6], coeff_by_sum[0..12]) + = sum_{i=0..6} sum_{j=0..6} + coeff_by_sum[i + j] * lhs[i] * rhs[j] mod Fr +``` + +It structurally corresponds to: + +```text +pair_wise_prod(xs, ys) +sum_exprs(double_base_powers, pair_wise_prod(xs, ys)) +``` + +from the foreign-field multiplication gates. The opcode is generic enough for +the foreign-field EC gates too, including `is_on_curve`, lambda-slope, +`assert_tangent`, and `assert_lambda_squared`, whenever their lowered +expressions contain the same 7-limb bilinear shape. + +This pass deliberately does not add SHA256, SHA512, RIPEMD, or spread +decomposition recognizers. Those chips are outside the current IVC verifier +scope, so recognizing them would add codegen surface without a measured win. + +These helpers therefore relate to these specific Rust circuit gates: + +| Helper/opcode | Rust gate source | Meaning | +|---|---|---| +| `q_pow5` | Poseidon full/partial/skipped-round gates | Poseidon S-box `x^5` | +| `q_limb7` | foreign-field normalization and multiplication | base-power limb packing | +| `q_limb7_wide` | foreign-field multiplication | double-base pairwise-product packing | +| `LIN7` | foreign-field normalization/multiplication and EC gates | table-backed 7-term linear limb packing | +| `BILIN7_ROW` | foreign-field multiplication and EC gates | one limb times a 7-limb weighted row | +| `BILIN7_PAIRWISE` | foreign-field multiplication and EC gates | 7-by-7 double-base bilinear convolution | + +## Failure Modes + +The evaluator reverts if: + +- calldata length does not equal `QUOTIENT_FRAME_LEN`; +- `y` is zero when selector inverse batching is needed; +- the modular exponentiation precompile used for `y^-1` fails; +- the quotient VM sees an invalid opcode or malformed native callback index; +- a generated arithmetic path explicitly detects an impossible state. + +Generated quotient VM bytecode is also decoded during code generation before it +is pinned into the VK payload. That offline pass rejects unknown opcodes, +truncated operands, unknown memory tokens, stack underflow, native-callback stack +leaks, and non-empty fold boundaries, so production Yul does not need those +checks in the hot path. + +The main verifier reverts if: + +- the quotient contract length/codehash is wrong; +- the staticcall fails; +- returned data length is wrong; +- returned magic is wrong. + +Invalid proof handling is therefore success-or-revert for this path. + +## Trace And Checkpoint Coverage + +The IVC trace-equivalence test compares native Rust verifier trace points +against generated Solidity trace logs for the main verifier path. The critical +quotient-facing trace points include: + +- `x`; +- `x^n`; +- `(x^n - 1)^-1`; +- `l_last`; +- `l_blind`; +- `l_0`; +- public instance evaluation; +- `linearization_expected_eval`; +- linearization scalars. + +The proof evaluation stream is also traced by the main verifier, including the +prover-supplied `q_evals` that are read after `x3`. Those values are compared +against the instrumented Rust verifier like the other proof scalar reads. + +There is one important external-evaluator boundary. In the IVC bench the +quotient numerator is reconstructed in `Halo2QuotientEvaluator` via +`STATICCALL`. Because EVM logs are forbidden under `STATICCALL`, the evaluator's +internal quotient identity trace hooks are deliberately logless. The IVC trace +test therefore excludes the per-identity quotient-evaluation trace id range: + +```text +30_000..40_000 +``` + +For that external path, trace equivalence still checks the reconstructed scalar +returned to the main verifier: + +- trace id `23`: `linearization_expected_eval = -nu_y(x)`; +- trace id `36`: reconstructed numerator `nu_y(x)`; +- trace ids `60_000..61_000`: selector accumulator folds. + +Monolithic and non-IVC trace tests do not cross the external `STATICCALL` +boundary, so they require the `30_000..40_000` quotient identity-evaluation +range and compare those internal batched evaluations directly. + +The gas-checkpoint bench reports the evaluator work under: + +```text +batched identity numerator reconstruction +``` + +For the current gas-capped compact profile with native lookup enabled, the +latest recorded IVC gas-checkpoint bench was: + +```text +gas-checkpoint tx gas: 1,499,730 +checkpointed section work: 1,336,950 +batched numerator section: 509,032 +verifier runtime: 12,592 bytes +VK runtime: 13,024 bytes +quotient evaluator runtime: 24,278 bytes +``` + +Defaulting summary: + +- gas-checkpoint tx gas: `1,499,730`; +- quotient runtime: `24,278` bytes. + +The checkpoint build includes debug logs, so its total gas is not identical to +the non-checkpoint trace-equivalence run. Use it for section deltas and +regression comparison, not as the production gas number. + +### Why A Compile-Stable Profile Was Around 700k Gas + +A historical `~700k` batched-numerator measurement came from using the smaller +compile-stable compact quotient VM profile instead of the older pre-native +lookup gas-capped compact profile. + +The old `~631k` run used: + +```text +direct inline identities: 4 +native gate callbacks: 4 +native permutation: on +native lookup: off +structured trash suffix: on +``` + +The compile-stable profile used: + +```text +direct inline identities: 0 +native gate callbacks: 4 +native permutation: on +native lookup: off +structured trash suffix: off +remaining identities: q_program VM +``` + +That change moved more numerator arithmetic through the interpreted +`q_program` VM. The VM saves bytecode, but every interpreted identity pays +dispatch, stack/memory traffic, and fold-state loads/stores. The measured trade +was: + +```text +batched numerator section: 631,289 -> 726,361 gas +quotient evaluator runtime: 21,774 -> 18,385 bytes +``` + +The non-trace IVC docs recorded the same band: + +```text +batched identity numerator reconstruction: 705,271 gas +quotient runtime: 18,318 bytes +``` + +The change was introduced by `c9b1567 Make verifier variants compile in CI`, +which temporarily changed the direct-prefix and trash-tail defaults. + +So the `~700k` number was not mainly a proof-shape or fewer-point-sets effect. +It was the bytecode/compile-stability tradeoff. This repository now emits the +gas-capped IVC shape directly, with native lookup enabled by default. + +## Defaulting Policy + +The default is gas-capped compact mode: + +```text +direct inline identities: 4 +native gate callbacks: 4 +native permutation: on +native lookup: on +structured trash suffix: on +remaining identities: q_program VM +``` + +This default was selected to keep the IVC quotient-numerator gas at or below +the previous `~631k` band while staying below the external quotient evaluator +size budget: + +- every deployed runtime, including the quotient evaluator, is below `24,576` + bytes; +- Rust/Solidity trace equivalence passes byte-for-byte. + +The default constants are intentionally fixed in code rather than exposed as +generation knobs. Changes to the shape should be made as code changes and must +pass the full trace-equivalence test, the variant-size check, and the IVC bench. +For example, `N=3` produced much smaller bytecode in one trial but failed trace +equivalence, so it is not a valid default. + +## Commands + +Run the detailed IVC gas bench: + +```bash +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ +scripts/run_ivc_bench.sh +``` + +Run the full IVC Rust/Solidity trace-equivalence test: + +```bash +SRS_DIR=/Users/Julien.Coolen/midfall/zk_stdlib/examples/assets \ +HALO2_SOLIDITY_RUN_IVC_BENCH=1 \ +cargo test --release \ + --features evm,rust-verifier-trace,truncated-challenges,in-circuit-fewer-point-sets \ + --test ivc_keccak_solidity ivc_final_keccak_solidity_e2e \ + -- --nocapture +``` diff --git a/proofs/solidity-verifier/docs/reference/REPRODUCIBLE_BUILDS.md b/proofs/solidity-verifier/docs/reference/REPRODUCIBLE_BUILDS.md new file mode 100644 index 000000000..2134df9a2 --- /dev/null +++ b/proofs/solidity-verifier/docs/reference/REPRODUCIBLE_BUILDS.md @@ -0,0 +1,119 @@ +# Reproducible Builds + +This repository pins the generated Solidity verifier build inputs that affect +bytecode: + +- Rust toolchain: `rust-toolchain.toml` +- Midfall dependency revision: + `53dc872f495104046d96bdac0a690f903dc0c537` +- Solidity compiler: `solc 0.8.30+commit.73712a01` +- Solidity compile flags: `--bin --optimize --via-ir --evm-version cancun --no-cbor-metadata` + +Repository-local `.cargo/config.toml` path overrides are intentionally not used. +All Midfall crates are resolved from the pinned git revision in `Cargo.toml`. + +## Canonical IVC Bench Command + +The current default IVC Solidity bench uses the multi-limb outer decider proof +layout: + +```bash +scripts/run_ivc_bench.sh --skip-srs-download +``` + +That command assumes `.srs/` already contains: + +- `midnight-srs-2p19` for the leaf IVC proofs; +- `midnight-srs-2p20` for the decider base SRS. + +Without `--skip-srs-download`, the script downloads missing Midnight SRS assets +it needs. With `--skip-srs-download`, it fails before compiling if any required +asset is missing. + +The default run compiles and runs the final Solidity decider bench directly. + +The final decider invocation uses: + +- features: + `evm,truncated-challenges,in-circuit-fewer-point-sets,solidity-gas-checkpoints` +- `SRS_DIR=./.srs` via the bench script default +- `solc optimize runs: 1` +- CBOR metadata omitted + +Opt into the single-H outer proof shape with: + +```bash +scripts/run_ivc_bench.sh --skip-srs-download --outer-single-h-commitment +``` + +That profile also needs `midnight-srs-2p22` for the extended monomial SRS and +runs a preliminary multi-limb leaf-bundle generator phase. + +## Recorded Optional Outer Single-H Runtime Hashes + +The concrete hashes below were generated with the fixed default quotient codegen shape: + +```bash +scripts/run_ivc_bench.sh \ + --outer-single-h-commitment \ + --skip-srs-download +``` + +That command uses: + +- features: + `evm,truncated-challenges,in-circuit-fewer-point-sets,outer-single-h-commitment,solidity-gas-checkpoints` +- `SRS_DIR=./.srs` via the bench script default +- `solc optimize runs: 1` +- CBOR metadata omitted + +Published deployed-runtime hashes: + +| Artifact | Runtime bytes | Runtime `keccak256` | +| --- | ---: | --- | +| `Halo2Verifier` | 11,962 | `0x8772fd9c1c75fbc2bbf6699778e3a19004b05c386536d1026b157a1b254802a5` | +| `Halo2VerifyingKey` | 14,016 | `0xd081cc83b30d045cb208f3045bd3d8ed6eb3ba582f44bc8358128319fc535363` | +| `Halo2QuotientEvaluator` | 23,221 | `0x4f4ea4e626f795dece8a739204772e0a25f91a6ea8ff0524656d079a84167b6f` | + +Total deployed runtime bytes: `49,199`. + +The same run accepted the final IVC Keccak proof on-chain in `1,374,697` gas. +The proof repacked from `4,912` compressed bytes to `7,392` padded bytes, with +`7,972` bytes of calldata. + +## Recorded Legacy Multi-Limb Runtime Hashes + +The concrete hashes below are the latest recorded default multi-limb profile in +this repository. They were generated with the fixed default quotient codegen shape: + +```bash +scripts/run_ivc_bench.sh \ + --skip-srs-download +``` + +That command uses: + +- features: + `evm,truncated-challenges,in-circuit-fewer-point-sets,solidity-gas-checkpoints` +- `SRS_DIR=./.srs` via the bench script default +- `solc optimize runs: 1` +- CBOR metadata omitted + +Published deployed-runtime hashes: + +| Artifact | Runtime bytes | Runtime `keccak256` | +| --- | ---: | --- | +| `Halo2Verifier` | 12,061 | `0xf0d2433a3142294afb4d9a9434d623b8f00bbea212380777b5aee197e79b1454` | +| `Halo2VerifyingKey` | 14,016 | `0x0f858e789c9d52f7e11beb96dd39fa30712c42f629c3505be9cceef3225119a0` | +| `Halo2QuotientEvaluator` | 23,221 | `0x1f8acc8aa363e10d031e9e56dd4180a70c714330fa466d3423f9ce1d4e1f00b2` | + +Total deployed runtime bytes: `49,298`. + +The same run accepted the final IVC Keccak proof on-chain in `1,399,268` gas. +The proof repacked from `5,056` compressed bytes to `7,776` padded bytes, with +`8,356` bytes of calldata. + +Compared with this multi-limb profile, outer single-H removes three quotient G1 +commitments: proof size drops by `144` compressed bytes and `384` padded bytes, +PCS block 5 drops from `533,202` to `515,290` gas, and total transaction gas +drops by `24,571`. diff --git a/proofs/solidity-verifier/docs/reference/STATUS.md b/proofs/solidity-verifier/docs/reference/STATUS.md new file mode 100644 index 000000000..ebf5589f8 --- /dev/null +++ b/proofs/solidity-verifier/docs/reference/STATUS.md @@ -0,0 +1,257 @@ +# Solidity Verifier Fidelity Status + +Assessed on 2026-05-11 against: + +- Solidity verifier path: `proofs/solidity-verifier` +- Midfall Rust verifier: `proofs/src` +- Midfall dependency source: local workspace path dependencies + +The release-facing justification and required evidence gates are consolidated in +[`docs/audit/CODEGEN_ASSURANCE_DOSSIER.md`](../audit/CODEGEN_ASSURANCE_DOSSIER.md). +`AUDIT_FINDINGS.md` remains the open-issues ledger; the current codegen +correctness claim depends on every finding there being fixed, covered by tests, +or explicitly excluded from production scope. + +## Verdict + +The Solidity verifier is a faithful specialized lowering of the pinned Midfall +Rust verifier for this repo's supported proof shape. It is not a generic +reimplementation of `midfall/proofs/src`. + +For the supported profile it preserves the Rust verifier's proof-read order, +Fiat-Shamir challenge order, quotient identity algebra, linearization, KZG +batching, and final pairing equation. The main divergences are deliberate: + +- the constraint system is compiled away; +- the verifying key is pinned as deployed bytecode plus digest/constants; +- public instance handling is narrowed; +- proof bytes are repacked for EIP-2537; +- the IVC carried accumulator check is extra wrapper logic beyond the Rust + PLONK verifier. + +The supported profile is approximately: + +- `F = midnight_curves::Fq`; +- `CS = KZGCommitmentScheme`; +- Keccak transcript; +- committed-instances feature enabled; +- one proof; +- exactly one committed instance column and one non-committed instance column; +- no rotated non-committed instance queries; +- quotient limbs equal to `cs.degree() - 1`; +- matching `truncated-challenges` / `outer-fewer-point-sets` features; +- BLS12-381 EIP-2537 precompiles available. + +## One-To-One Map + +| Rust verifier operation | Solidity generator / template | Fidelity notes | +|---|---|---| +| `parse_trace` absorbs VK, instances, commitments, and challenges in order: `../midfall/proofs/src/plonk/verifier.rs:24` | Protocol schedule: `src/lowering/protocol/mod.rs:241`; transcript template: `templates/contracts/Halo2Verifier.sol:1031` | Same order for the supported profile. Solidity always assumes one committed identity instance and one public instance column. | +| VK hash via `VerifyingKey::hash_into`: `../midfall/proofs/src/plonk/mod.rs:282` | VK digest generation: `src/lowering/vk.rs`; runtime load/check: `templates/contracts/Halo2Verifier.sol:979` | Transcript absorbs the same digest, but Solidity trusts generated/pinned VK constants instead of reconstructing the Rust VK/CS. | +| Dynamic `ConstraintSystem` and `Expression::evaluate`: `../midfall/proofs/src/plonk/circuit.rs:825` | Compile-time metadata/layout: `src/lowering/encoding/mod.rs:60`; expression lowering: `src/lowering/quotient_numerator/yul_emit.rs:780` | The CS is not present on-chain. Expressions are pre-lowered from the pinned CS. Faithful only if generated from the exact VK/CS being verified. | +| Master gate / permutation / lookup / trash identities via `partially_evaluate_identities`: `../midfall/proofs/src/plonk/mod.rs:488` | Quotient identity construction: `src/lowering/quotient.rs`; evaluator blocks: `src/lowering/quotient_numerator/yul_emit.rs`; quotient Yul: `templates/partials/quotient_numerator/QuotientNumeratorBlock.yul:1` | Algebra matches. Simple selectors are handled structurally differently: Rust returns selector buckets; Solidity omits selector evals from proof and accumulates selector commitments in the fused MSM. | +| Linearization commitment: `../midfall/proofs/src/plonk/linearization/verifier.rs:45` | Linearization scalar prep: `templates/contracts/Halo2Verifier.sol:1450`; PCS fused expansion: `src/lowering/kzg/mod.rs:1304` | Same commitment/evaluation relation, but Solidity fuses terms directly into the final MSM instead of materializing the full linearization object. | +| KZG `multi_prepare`: `../midfall/proofs/src/poly/kzg/mod.rs:320` | Query schedule and PCS codegen: `src/lowering/kzg/mod.rs:83`; emitted PCS blocks: `src/lowering/kzg/mod.rs:602` | Same batching algebra: dummy queries, point sets, `x1/x2/x3/x4`, `f_com`, `q_evals`, `pi`. Solidity uses optimized direct formulas, batch inversions, and fused MSMs. | +| Final `DualMSM::check`: `../midfall/proofs/src/poly/kzg/msm.rs:282` | Pairing inputs in PCS: `src/lowering/kzg/mod.rs:1484`; final pairing: `templates/contracts/Halo2Verifier.sol:1723` | Equivalent equation. Solidity passes arguments swapped because its helper checks `(rhs, G2)` and `(lhs, -sG2)`. | + +## CS And VK Handling + +Rust keeps a full `VerifyingKey` with a live `ConstraintSystem`: + +- `VerifyingKey` stores fixed commitments, permutation commitments, domain, + and CS data. +- `VerifyingKey::from_parts` computes a transcript representation that binds + the pinned CS debug form. +- `hash_into` absorbs that representation into the transcript. + +Solidity splits this into: + +- a generated VK payload containing digest, domain constants, fixed + commitments, permutation commitments, G2 constants, quotient metadata, and + optional accumulator metadata; +- runtime bytecode length/codehash checks for the VK and optional quotient + evaluator; +- compile-time expression lowering from the Rust CS into Yul. + +This is faithful under codehash pinning. It is not a runtime reconstruction of +the Rust VK or CS. If the VK bytecode, digest, constants, or external quotient +program were allowed to drift independently, the Solidity verifier could accept +proofs under inconsistent metadata. + +## Master Gate Equation Evaluation + +Rust evaluates gate expressions dynamically through `Expression::evaluate`, +using closures for fixed, advice, instance, challenge, negation, sum, product, +and scaling. `partially_evaluate_identities` then chains: + +- gate identities; +- permutation identities; +- lookup/logup identities; +- trash identities. + +Solidity pre-lowers this into codegen blocks: + +- `Evaluator::gate_computations_tagged`; +- `Evaluator::permutation_computations`; +- `Evaluator::lookup_computations`; +- `Evaluator::trashcan_computations`; +- `QuotientNumeratorBlock.yul` or an external quotient evaluator. + +The algebra is the same, but Solidity uses structural optimizations: + +- common-subexpression elimination; +- special-case recognition for helper expressions such as pow5 and 7-limb + decomposition; +- prefix/suffix product tricks for lookup helper evaluations; +- selector bucket accumulation instead of proving simple selector evals. + +These are code-generation changes, not protocol changes, assuming the generated +program is pinned to the same CS. + +## PCS And Batching + +Rust `KZGCommitmentScheme::multi_prepare` does the following: + +1. Optionally appends dummy queries. +2. Samples `x1` and `x2`. +3. Constructs and sorts intermediate point sets. +4. Folds commitments and evaluations into `q_com` and `q_eval_set` using + powers of `x1`. +5. Reads `f_com`. +6. Samples `x3`, optionally truncating it. +7. Reads `q_evals`. +8. Computes `f_eval`. +9. Samples `x4`. +10. Computes `final_com`, `v`, and the final `DualMSM`. +11. Reads `pi`. + +Solidity mirrors this in `src/lowering/kzg/mod.rs` and the generated Yul, with these +implementation differences: + +- rotations are precomputed and laid out in memory; +- intermediate sets are simulated at lowering time; +- `q_eval_set` and `q_com` traces are materialized in optimized memory slots; +- `f_eval` is computed via direct Lagrange-at-`x3` formulas rather than + constructing an interpolation polynomial; +- final commitment accumulation is fused into one MSM; +- identity point terms are skipped where safe; +- `truncated-challenges` handling masks the challenge/powers in the same way as + the Rust feature-gated verifier. + +The batching algebra is faithful when the Rust and Solidity feature flags match. +Feature mismatches change the proof layout and challenge algebra. + +## Final Pairing Check + +Rust constructs: + +- `left = pi`; +- `right = final_com - v * G + x3 * pi`; + +and checks the KZG pairing equation through `DualMSM::check`. + +Solidity computes the same two G1 values. The final call is swapped at the +template boundary because the Solidity helper checks `(arg0, G2)` and +`(arg1, -sG2)`. This is algebraically equivalent to the Rust equation. + +## IVC Carried Accumulator Check + +The carried proof accumulator pairing check is not part of +`midfall/proofs/src/plonk/verifier.rs`. It is Solidity-generator wrapper logic +layered on top of the PLONK verifier. + +The relevant pieces are: + +- `AccumulatorEncoding` in `src/lowering/mod.rs`; +- accumulator metadata in the generated VK; +- decoding helpers in `templates/contracts/Halo2Verifier.sol`; +- final accumulator batching in `templates/contracts/Halo2Verifier.sol`. + +The current supported accumulator shape is a fully collapsed public accumulator +encoded as 7 limbs of 56 bits per coordinate, with shifted coordinate packing. +The template reconstructs EIP-2537 coordinates from public instance limbs and +uses the final pairing precompile to batch-check the carried accumulator +equation together with the KZG equation. + +The current working tree contains an important hardening change in +`templates/contracts/Halo2Verifier.sol`: decoded accumulator LHS/RHS points are routed +through G1MSM even on identity, zero-scalar, and one-scalar paths. That prevents +malformed public accumulator points from being erased before EIP-2537 validates +them. + +The accumulator check samples: + +```text +alpha = keccak(domain || KZG rhs/lhs || accumulator rhs/lhs) mod r +``` + +then adds `alpha * accumulator_rhs` and `alpha * accumulator_lhs` into the +existing KZG pairing inputs. This is a sound random linear combination of the +two pairing equations, assuming the public inputs are bound by the surrounding +application and the accumulator encoding metadata matches the generated +verifier. + +Because this check is extra Solidity wrapper logic, the Solidity verifier is +best described as: + +```text +Rust PLONK verifier, specialized and lowered to Solidity ++ IVC carried accumulator post-check +``` + +not exactly the same function as Rust `prepare` / `multi_prepare`. + +## Main Caveats + +1. Generic committed instance commitments are unsupported. Solidity assumes the + committed instance commitment is the identity point. + +2. Generic instance-column shapes are unsupported. The generator validates + exactly one committed instance column and one non-committed instance column. + +3. Rotated non-committed instance queries are unsupported. + +4. Multi-proof verification is not represented generically. The codegen targets + the single-proof shape. + +5. The on-chain verifier trusts pinned VK bytecode and quotient bytecode. The + transcript absorbs the VK digest as Rust does, but runtime constants are not + rederived from first principles. + +6. The Solidity proof format is not the native Rust proof format. The repacker + is critical: Rust consumes compressed G1 points and native scalar encoding; + Solidity consumes EIP-2537-compatible uncompressed G1 words and big-endian + scalars. + +7. Curve and subgroup validation relies on EIP-2537 precompiles. The transcript + can absorb point bytes before the corresponding precompile operation rejects + malformed points, although final success remains gated on precompile success. + +8. Feature mismatches such as `truncated-challenges` or + `outer-fewer-point-sets` break equivalence. + +## Evidence And Test Coverage + +Existing tests provide strong evidence for the current supported profile: + +- valid proof/native prepare and Solidity acceptance tests in `src/test.rs`; +- Rust/Solidity trace-equivalence tests in `src/test.rs`; +- required protocol coverage checks in `src/test.rs`; +- IVC Solidity tests in `tests/ivc_keccak_solidity.rs`, including bad + accumulator packing and malformed accumulator point cases; +- accumulator schema/metadata/packing tests in `src/lowering/mod.rs`. + +This status note is based on static source mapping. The suite was not rerun as +part of this pass. + +## Bottom Line + +For the pinned Midfall profile, the Solidity verifier is structurally and +algebraically faithful to the Rust verifier. It follows the same operation order +through quotient evaluation, linearization, KZG batching, and the final pairing +check. + +It should not be treated as a generic Halo2/Midfall verifier. Its soundness +argument depends on the narrow supported proof shape, exact feature matching, +correct proof repacking, VK/quotient codehash pinning, and the extra IVC +accumulator wrapper semantics being the intended application semantics. diff --git a/proofs/solidity-verifier/docs/reference/TEAM_DEMO_SETUP.md b/proofs/solidity-verifier/docs/reference/TEAM_DEMO_SETUP.md new file mode 100644 index 000000000..e0a197899 --- /dev/null +++ b/proofs/solidity-verifier/docs/reference/TEAM_DEMO_SETUP.md @@ -0,0 +1,478 @@ +# Team Demo Setup: Halo2 Solidity IVC Verifier + +Use this guide to prepare a fresh laptop before the demo session and to replay +the same commands during the meeting. + +The demo shows the optimized IVC Solidity verifier path where the quotient +numerator body is small enough to live inside `Halo2Verifier` instead of being +deployed as a separate `Halo2QuotientEvaluator` contract. For background, read +`docs/reference/QUOTIENT_EVALUATOR_9KB_BYTECODE.md`. + +## Assumptions + +- macOS, Linux, or Windows with WSL2 Ubuntu. +- A stable internet connection. +- At least 20 GB free disk space. +- A terminal. +- GitHub access to the repository. +- Git, Rust/Cargo, and a normal native build toolchain are already installed. + +The full proof bench is CPU-heavy. Please do the preparation before the meeting +so the session can focus on reading the generated contracts and comparing gas +numbers. + +## 1. Clone the Repo + +Use SSH if your GitHub account is set up for it: + +```bash +git clone git@github.com:privacy-ethereum/halo2-solidity-verifier.git +cd halo2-solidity-verifier +``` + +Or use HTTPS: + +```bash +git clone https://github.com/privacy-ethereum/halo2-solidity-verifier.git +cd halo2-solidity-verifier +``` + +Switch to the demo branch once it has been pushed: + +```bash +git fetch origin +git switch next2 +``` + +Check that you are in the right place: + +```bash +git status +``` + +## 2. Install the Pinned Solidity Compiler + +The benchmark is reproducible only with `solc 0.8.30+commit.73712a01`. +Use the repo helper: + +```bash +scripts/install_pinned_solc.sh .solc +export SOLC="$PWD/.solc/solc" +``` + +Check: + +```bash +"$SOLC" --version +``` + +Expected version prefix: + +```text +Version: 0.8.30+commit.73712a01 +``` + +If you open a new terminal later, run this again from the repo root: + +```bash +export SOLC="$PWD/.solc/solc" +``` + +## One-Command Demo Runner + +The scripted path performs the setup checks, installs pinned `solc` when needed, +checks/downloads SRS assets, checks the sibling Moonlight checkout, prints +colored progress logs, and writes full command logs under +`target/team-demo-logs`. + +Run setup and compile preflights only: + +```bash +scripts/run_team_demo.sh --check-only +``` + +Run the full demo path: + +```bash +scripts/run_team_demo.sh +``` + +For local exploratory runs that should use whatever `solc` and Rust toolchain +are already installed, opt out of the pinned setup: + +```bash +scripts/run_team_demo.sh --allow-unpinned-solc --rust-toolchain stable +``` + +Those runs are useful for smoke-testing, but the resulting bytecode and gas +numbers are not reproducible demo numbers. + +The full path runs the IVC Rust/Solidity trace equivalence test without the +detailed gas checkpoint bench, then runs the Moonlight wrap decider Solidity +bench and trace equivalence check. Use `--help` to see options for custom SRS +directories, HTTPS Moonlight cloning, or skipping one of the heavy runs. + +## 3. Download SRS Assets + +For the demo, we will use the multi-limb outer proof shape so everyone can +replicate the same numbers from the session: + +```bash +scripts/run_ivc_bench.sh --check-only +``` + +This command downloads the required Midnight SRS files into `.srs/` and compiles +the IVC bench without running the expensive proof. It may take a while on a +fresh laptop because Cargo has to build the dependency graph. + +If the command succeeds, your laptop is ready for the session. + +## 4. Quick Sanity Check + +Run the focused codegen tests: + +```bash +cargo test --features evm,truncated-challenges,in-circuit-fewer-point-sets --lib codegen +``` + +Expected ending: + +```text +test result: ok. +``` + +## Commands We Will Run During the Demo + +Start from the repo root: + +```bash +cd halo2-solidity-verifier +export SOLC="$PWD/.solc/solc" +git pull +``` + +Run the full IVC Solidity verifier bench: + +```bash +scripts/run_ivc_bench.sh --skip-srs-download +``` + +Use the local unpinned toolchain instead: + +```bash +scripts/run_ivc_bench.sh --skip-srs-download \ + --allow-unpinned-solc \ + --rust-toolchain stable +``` + +By default this proves, renders Solidity, compiles with pinned `solc`, deploys +into local Prague `revm`, verifies the proof, and prints gas checkpoints. + +On a machine similar to the demo laptop, this run took around 3-5 minutes after +the prep build. Slower laptops can take longer. + +## Moonlight Wrap Proof Solidity Verifier + +The Moonlight demo proves the final wrap recursion proof from +`wrap_circuit_composes_two_fold_children_from_four_dummy_fold_proofs`, renders a +Solidity verifier with this repository, deploys it into local Prague `revm`, and +verifies the Moonlight proof on-chain. + +Clone Moonlight next to this verifier repo: + +```bash +cd .. +git clone git@github.com:EYBlockchain/Moonlight.git +cd Moonlight +git fetch origin +git switch codex/wrap-bench-cherry-picks +``` + +If you need HTTPS instead: + +```bash +cd .. +git clone https://github.com/EYBlockchain/Moonlight.git +cd Moonlight +git fetch origin +git switch codex/wrap-bench-cherry-picks +``` + +The Moonlight branch pins its Midnight dependencies to the same Midfall commit +as this verifier repository and uses this local checkout through: + +```text +halo2_solidity_verifier = { path = "../../halo2-solidity-verifier-exp", ... } +``` + +That path assumes the two repositories are siblings: + +```text +parent/ + halo2-solidity-verifier-exp/ + Moonlight/ +``` + +If your checkout names or locations differ, the one-command runner checks this +path and creates a local symlink from Moonlight's expected verifier dependency +path to the current checkout when the target is missing. Pass +`--no-fix-moonlight-dep` if you prefer the script to fail instead. If you run +the Cargo command directly, either keep the sibling checkout name above or +update/create the dependency path yourself before running the bench. + +From this verifier repo, run the Moonlight Solidity path with the sibling +Moonlight manifest path: + +```bash +export SOLC="$PWD/.solc/solc" + +MOONLIGHT_RUN_WRAP_SOLIDITY_BENCH=1 \ +MOONLIGHT_RUN_WRAP_SOLIDITY_TRACE=1 \ +cargo test --manifest-path ../Moonlight/aggregation/Cargo.toml \ + wrap_circuit_composes_two_fold_children_from_four_dummy_fold_proofs --release \ + --lib -- --ignored --nocapture +``` + +Use local unpinned tooling instead: + +```bash +HALO2_SOLIDITY_ALLOW_UNPINNED_SOLC=1 \ +RUSTUP_TOOLCHAIN=stable \ +MOONLIGHT_RUN_WRAP_SOLIDITY_BENCH=1 \ +MOONLIGHT_RUN_WRAP_SOLIDITY_TRACE=1 \ +cargo test --manifest-path ../Moonlight/aggregation/Cargo.toml \ + wrap_circuit_composes_two_fold_children_from_four_dummy_fold_proofs --release \ + --lib -- --ignored --nocapture +``` + +The important part is that Cargo targets Moonlight's `aggregation/Cargo.toml` +while Moonlight's dev dependency points back to this local Solidity verifier +checkout. + +The run is much heavier than the IVC bench. On the demo machine it took about +23 minutes. The final proof is produced with Midfall's +`CircuitTranscript` and with the final outer proof layout set +to no fewer-point-sets so it matches the generated Solidity verifier. +The Solidity path also enables the public accumulator check. Moonlight exposes +its final wrap accumulator as an already-collapsed `(lhs, rhs)` point pair at +instance offset `11`, so the verifier uses +`AccumulatorEncoding::point_pair(offset, 7, 56)` rather than the Midfall IVC +`point, scalar, point, scalar` layout. + +Expected success line: + +```text +[moonlight-wrap-solidity] PASS: final wrap decider proof accepted on-chain +``` + +The run also prints an IVC-style gas checkpoint table with section names, gas +deltas, percentages, measurement overhead, and total transaction gas. With the +public accumulator check enabled, the local demo run was: + +```text +[moonlight-wrap-solidity][gas] inferred PCS point sets = 4 + 15 4,998,802,662 64,564 5.6% public accumulator pairing batch prep + total tx gas_used = 1,302,138 (incl. tx base + calldata + pre-cp1 + post-last) +[moonlight-wrap-solidity] PASS: final wrap decider proof accepted on-chain in 1302138 gas +[moonlight-wrap-solidity][trace] matched 244 native Rust/Solidity trace points; generator-only accumulator trace ids: [29, 30] +[moonlight-wrap-solidity][trace] PASS: final wrap decider trace matched native Midfall and accepted on-chain +``` + +By default, generated Moonlight verifier artifacts are written under: + +```text +../Moonlight/aggregation/target/moonlight-wrap-solidity-dump +``` + +To keep the artifacts under this verifier repo instead: + +```bash +MOONLIGHT_RUN_WRAP_SOLIDITY_BENCH=1 \ +MOONLIGHT_RUN_WRAP_SOLIDITY_TRACE=1 \ +MOONLIGHT_WRAP_SOLIDITY_DUMP_DIR=target/moonlight-wrap-solidity-dump \ +cargo test --manifest-path ../Moonlight/aggregation/Cargo.toml \ + wrap_circuit_composes_two_fold_children_from_four_dummy_fold_proofs --release \ + --lib -- --ignored --nocapture +``` + +The dump includes both the production gas-bench verifier and the trace verifier: + +```text +Halo2Verifier.sol +Halo2VerifyingKey.sol +Halo2Verifier.trace.sol +Halo2VerifyingKey.trace.sol +proof.bin +instance.le +calldata.bin +``` + +## Expected Demo Output + +Look for: + +```text +[ivc-keccak-solidity] PASS: IVC final Keccak proof accepted on-chain +``` + +Then inspect the generated contract-size summary: + +```bash +cat target/ivc-keccak-solidity-dump/contract-sizes.txt +``` + +For the split verifier/quotient-evaluator shape with the multi-limb outer proof +layout, the demo run should be close to: + +```text +Halo2Verifier deployed runtime bytes: 16463 +Halo2VerifyingKey deployed runtime bytes: 15136 +Halo2QuotientEvaluator deployed runtime bytes: 18318 +total deployed runtime bytes: 49917 +``` + +Small differences can happen if the branch changes before the session. The +important checks are: + +- `Halo2Verifier` stays below the EIP-170 limit of `24576` bytes. +- `Halo2QuotientEvaluator` stays below the EIP-170 limit of `24576` bytes. +- The proof is accepted on-chain in the local EVM. +- The generated dump contains both `Halo2Verifier.sol` and + `Halo2QuotientEvaluator.sol`. + +Check the generated files: + +```bash +ls target/ivc-keccak-solidity-dump +``` + +Expected key files: + +```text +Halo2Verifier.sol +Halo2VerifyingKey.sol +Halo2Verifier.creation.bin +Halo2VerifyingKey.creation.bin +contract-sizes.txt +calldata.bin +proof.bin +``` + +## Inspect the Optimized Quotient Body + +Search for the compact quotient VM section inside the generated verifier: + +```bash +grep -nE "Compact quotient-program mode|native gate|Q_OP_NATIVE|q_program" \ + target/ivc-keccak-solidity-dump/Halo2Verifier.sol +``` + +Read the implementation notes: + +```bash +sed -n '1,220p' docs/reference/QUOTIENT_EVALUATOR_9KB_BYTECODE.md +``` + +## Replay Without Proving Again + +After one full bench run, you can replay the generated calldata and contracts +without regenerating the proof: + +```bash +cargo run --release \ + --features evm,truncated-challenges,fewer-point-sets \ + --example ivc_replay +``` + +This is useful during the session if we want to tweak generated Solidity and +quickly test deployment or verification behavior. + +## Optional Experiments + +Try the compact VM shape profile: + +Set `CodegenConfig::quotient_shape_profile` to `true`, then run +`scripts/run_ivc_bench.sh --skip-srs-download`. + +Try disabling native gate callbacks: + +Set `CodegenConfig::quotient_native_gates` to `0`, then run +`scripts/run_ivc_bench.sh --skip-srs-download`. + +Try changing the native-gate count: + +Set `CodegenConfig::quotient_native_gates` to `5`, then run +`scripts/run_ivc_bench.sh --skip-srs-download`. + +The native-gate selector is byte-budgeted, so increasing the count does not +automatically mean the verifier spends more runtime bytecode. + +## Troubleshooting + +### `solc version ... does not match pinned` + +Run: + +```bash +scripts/install_pinned_solc.sh .solc +export SOLC="$PWD/.solc/solc" +"$SOLC" --version +``` + +### Missing SRS asset + +Run prep again without `--skip-srs-download`: + +```bash +scripts/run_ivc_bench.sh --check-only +``` + +### Cargo cannot find Rust `1.90.0` + +```bash +rustup toolchain install 1.90.0 +``` + +### Build is slow + +The first build is the expensive one. Let the prep command finish before the +meeting. The full bench is faster once dependencies are compiled. + +### `manifest path aggregation/Cargo.toml does not exist` + +You are running the Moonlight command from this verifier repo while using a +Moonlight manifest path relative to Moonlight itself. From this verifier repo, +use the sibling repo path: + +```bash +cargo test --manifest-path ../Moonlight/aggregation/Cargo.toml \ + wrap_circuit_composes_two_fold_children_from_four_dummy_fold_proofs --release \ + --lib -- --ignored --nocapture +``` + +Or `cd ../Moonlight` and use `--manifest-path aggregation/Cargo.toml`. + +### Moonlight cannot find `halo2_solidity_verifier` + +Check the path dependency in `Moonlight/aggregation/Cargo.toml`. It should point +to this local verifier checkout. For sibling checkouts named as above, use: + +```text +halo2_solidity_verifier = { path = "../../halo2-solidity-verifier-exp", features = ["evm", "truncated-challenges", "solidity-gas-checkpoints"] } +``` + +### Apple Silicon `solc` will not run + +Install Rosetta: + +```bash +softwareupdate --install-rosetta --agree-to-license +``` + +Then reinstall pinned `solc`: + +```bash +rm -rf .solc +scripts/install_pinned_solc.sh .solc +export SOLC="$PWD/.solc/solc" +``` diff --git a/proofs/solidity-verifier/docs/reference/TESTING.md b/proofs/solidity-verifier/docs/reference/TESTING.md new file mode 100644 index 000000000..68a6f2566 --- /dev/null +++ b/proofs/solidity-verifier/docs/reference/TESTING.md @@ -0,0 +1,504 @@ +# Testing + +This document collects the commands needed to exercise the examples and the +property-based test (PBT) suite shipped with this crate. + +The release-facing assurance gates are summarized in +[`docs/audit/CODEGEN_ASSURANCE_DOSSIER.md`](../audit/CODEGEN_ASSURANCE_DOSSIER.md). +This file remains the operational runbook for those gates and the broader +negative-test suite. + +The workspace is pinned to the toolchain in [`rust-toolchain.toml`](./rust-toolchain.toml) +(currently Rust 1.90.0). Solidity-touching tests and examples additionally +require pinned `solc 0.8.30+commit.73712a01` on `PATH` or via `SOLC`. + +Midnight crates resolve from the surrounding Midfall workspace through local +path dependencies in `Cargo.toml`, so verifier bytecode tracks this branch of +`proofs`, `curves`, `circuits`, `aggregation`, and `zk_stdlib`. + +The ignored proving benches need local SRS files. `scripts/run_ivc_bench.sh` +downloads the IVC bench assets into `.srs/` by default, or you can point +`SRS_DIR` at an existing SRS directory. The IVC Solidity tree bench needs +Midnight `midnight-srs-2p19` for the leaf IVC proofs and +`midnight-srs-2p20` for the final decider proof. The optional outer single-H +decider proof also needs `midnight-srs-2p22` for the extended monomial basis. +The optional native Midfall comparison path also needs a Filecoin SRS. + +```bash +rustc --version # should report 1.90.0 +solc --version # should report 0.8.30+commit.73712a01 +``` + +--- + +## Examples + +`Cargo.toml` sets `autoexamples = false`, so stale pre-Midnight diagnostic +programs under `examples/` are not built by default. The maintained registered +example is the IVC replay harness: + +```bash +cargo run --release \ + --features evm,truncated-challenges,fewer-point-sets \ + --example ivc_replay +``` + +It loads contracts and calldata previously written by the ignored IVC bench at +`target/ivc-keccak-solidity-dump/`, recompiles them with `solc`, deploys them +in Prague-spec `revm`, and calls `verifyProof`. + +--- + +## Tests + +### Default suite (render + EVM smoke tests) + +These compile and run the verifier end-to-end with the default multi-prepare +KZG PCS emitter. They are not `#[ignore]`d and run as part of the default test +invocation: + +```bash +cargo test --workspace --all-features --all-targets -- --nocapture +``` + +### Property-based tests (PBT) + +The PBT cases are EVM-heavy and only make sense in `--release`. They self-skip +unless `HALO2_SOLIDITY_RUN_EVM_TESTS=1`, the pinned solc is available, and the +SRS assets are present. Run the whole PBT batch with: + +```bash +HALO2_SOLIDITY_RUN_EVM_TESTS=1 \ +SRS_DIR=/path/to/srs \ +cargo test -p halo2_solidity_verifier --release \ + --features evm,rust-verifier-trace \ + pbt_ -- --nocapture +``` + +Individual cases: + +```bash +# Positive case: real proofs are accepted by the embedded verifier. +cargo test -p halo2_solidity_verifier --release \ + --features evm,rust-verifier-trace \ + pbt_solidity_verifies_standard_plonk_embedded_vk_proofs \ + -- --nocapture + +# Negative: corrupted public inputs must be rejected (embedded + separate). +cargo test -p halo2_solidity_verifier --release \ + --features evm,rust-verifier-trace \ + pbt_solidity_rejects_wrong_instances \ + -- --nocapture + +# Negative: any single-bit flip in the proof bytes must be rejected. +cargo test -p halo2_solidity_verifier --release \ + --features evm,rust-verifier-trace \ + pbt_solidity_rejects_malleated_proofs \ + -- --nocapture + +# Negative: a separate verifier must reject a VK contract it was not pinned to. +cargo test -p halo2_solidity_verifier --release \ + --features evm,rust-verifier-trace \ + pbt_solidity_rejects_wrong_verifying_keys \ + -- --nocapture + +# Negative: even a one-nibble change inside vk_digest must break verification. +cargo test -p halo2_solidity_verifier --release \ + --features evm,rust-verifier-trace \ + pbt_separate_vk_digest_prefix_affects_verification \ + -- --nocapture +``` + +### Other Solidity/EVM-heavy tests + +These are also `#[ignore]`d but are not property tests. Run them all with: + +```bash +cargo test --release --all-features \ + -- --ignored --nocapture \ + malformed_embedded_calldata_variants_are_rejected \ + mutated_separate_vk_contract_is_rejected \ + standard_plonk_render_is_deterministic_for_same_seed \ + compile_solidity_is_deterministic_for_same_source +``` + +What each one covers: + +- `malformed_embedded_calldata_variants_are_rejected` — empty proof, truncated + proof, extra trailing bytes, wrong selector, wrong instance-array length. +- `mutated_separate_vk_contract_is_rejected` — flips a single nibble in the + first 64-byte hex literal of the VK source and asserts rejection. +- `standard_plonk_render_is_deterministic_for_same_seed` — same `(k, seed)` + must produce identical Solidity sources for both embedded and separate. +- `compile_solidity_is_deterministic_for_same_source` — same Solidity source + must compile to identical bytecode (smoke test for `solc` reproducibility). + +### IVC Keccak Solidity bench + +This slow ignored test proves two independent one-step IVC Poseidon hash-chain +leaves, proves a final Keccak-transcript tree decider that verifies both leaf +proofs and fully collapses the carried IVC proof accumulator, renders the +Solidity verifier/VK for that decider proof, deploys them in Prague-spec +`revm`, verifies on-chain, reports contract sizes, and prints section-level gas +checkpoints. + +Compile-only check: + +```bash +scripts/run_ivc_bench.sh --check-only +``` + +Full bench: + +```bash +scripts/run_ivc_bench.sh +``` + +The default full bench emits the final Keccak decider proof with the multi-limb +quotient commitment layout. To opt into the outer single-H decider proof shape: + +```bash +scripts/run_ivc_bench.sh --outer-single-h-commitment +``` + +Expected single-H deltas for the current IVC decider are small but visible: +the final PCS MSM shrinks from `78` to `75` terms. In the current profiled +command this moves PCS block 5 from `533,202` to `515,290` gas and total +transaction gas from `1,399,268` to `1,374,697`. The quotient numerator +checkpoint does not change, because single-H changes only the quotient +commitment side. + +The bench keeps fewer point sets for the recursive verifier only; the outer +Solidity-facing decider proof omits `outer-fewer-point-sets`. + +Use an existing SRS directory and also run the native Midfall final-proof twin: + +```bash +SRS_DIR=/path/to/midfall/zk_stdlib/examples/assets \ + scripts/run_ivc_bench.sh --native-midfall +``` + +Generated contracts, calldata, proof bytes, and size reports are written to: + +```text +target/ivc-keccak-solidity-dump/ +``` + +### Run absolutely everything + +```bash +cargo test --release --workspace --all-features --all-targets \ + -- --include-ignored --nocapture +``` + +Be aware: this can take several minutes because the PBT cases run multiple +proof-generation + EVM rounds per case. + +--- + +## Audit-Driven Halo2/KZG Solidity Test Checklist + +This checklist is source-grounded in verifier audit findings across PLONK, +Groth16, Halo2, and verifier code generators. It is tailored to this repository: +Halo2/KZG Solidity verifier generation over BLS12-381/EIP-2537 precompiles. +The broader production-readiness framing is also recorded in +[`ROADMAP.md`](./ROADMAP.md). + +### 1. Protocol And Trust Model + +Tests and fixtures must pin the exact protocol being checked: + +- Halo2 variant: KZG vs IPA, SHPLONK/multi-opening scheme, aggregation or + accumulator scheme, single proof vs batch proof. +- Transcript: challenge order, hash function, domain separators, encodings, + and challenge-to-field method. +- Proving-key/verifying-key relationship: domain size `k`, instance columns, + rotations, commitments, fixed/advice/lookup configuration. +- SRS/CRS assumption: ceremony, powers included, and whether the verifier is + bound to that SRS. + +Linea-style trusted-setup failures should be treated as critical: a forged or +unbound CRS can make proofs meaningless. + +### 2. VK, SRS, Circuit, And Chain Binding + +Negative tests should mutate or swap: + +- `vk_digest` / circuit digest / verifying-key hash. +- `[x]_2` or equivalent BLS G2 SRS element. +- Domain parameters: `n`, `omega`, `omega_inv`, `n_inv`, rotations, cosets. +- Public input count and ordering. +- Protocol version, curve ID, transcript version, proof layout version. +- Chain ID / verifier address for state-transition wrappers. + +Espresso and Scroll findings around missing SRS/curve/public-parameter +transcript binding should be covered by transcript and VK mutation tests. + +### 3. Calldata And Proof Layout + +Malformed calldata tests must cover: + +- Exact proof length; missing bytes and extra trailing bytes. +- Exact public input count. +- Dynamic ABI offset overlap and malformed dynamic heads. +- Unused appended bytes ignored by the verifier. +- Every fixed commitment/evaluation offset. +- Compressed vs uncompressed point parser separation. +- Explicit endian convention. +- BLS 48-byte Fq and 96-byte Fq2 handling, with no truncation into `uint256`. + +Proof layout is consensus-critical: the Solidity parser is part of the proof +system. + +### 4. Canonical Field Encodings + +Reject prover-supplied non-canonical values instead of reducing them unless the +reference protocol explicitly specifies reduction first. + +Boundary cases: + +- Public inputs `< Fr`. +- Scalar evaluations `< Fr`. +- MSM scalars `< Fr`. +- Challenges `< Fr`. +- Fq coordinates `< q`. +- Fq2 limbs `< q`. +- Compressed point encodings are canonical and unique. +- Reject `x + q`, `s + r`, `q`, `r`, and alternative encodings. + +Linea, Risc0, and Espresso findings all point to canonicalization tests as a +core negative-test family. + +### 5. Curve Points + +For every proof/VK/accumulator point slot, test: + +- canonical encoding; +- on-curve validation; +- subgroup membership or documented precompile guarantee; +- infinity policy; +- negation only after reduction and on-curve validation; +- compressed sign-bit canonicality; +- G2 Fq2 coefficient ordering against the exact off-chain library. + +BLS12-381 makes subgroup/cofactor and coordinate-width mistakes especially +expensive. + +### 6. BLS Precompile Wrappers + +Mock/fork tests should force precompile failure modes: + +- exact input length; +- exact output length; +- fresh or cleared output memory; +- checked `staticcall` success; +- checked `returndatasize()`; +- checked pairing result value; +- no unjustified `sub(gas(), 2000)`; +- no stale memory reuse after failure; +- target-chain/L2 behavior documented. + +Linea found invalid proofs passing when the pairing result was not checked, and +stale-memory bugs when `staticcall` failure was ignored. + +### 7. Finite-Field Arithmetic Edge Cases + +Add small/toy circuits or direct helper tests for: + +- `inv(0)` rejection; +- batch inversion with empty, singleton, and zero-containing ranges; +- Lagrange evaluation when `zeta` is a root of unity; +- `addmod`/`mulmod` usage for Fr arithmetic; +- no `uint256` arithmetic for BLS Fq; +- `n_inv * n = 1 mod Fr`; +- `omega^n = 1`; +- correct two-adicity, cosets, and rotation constants. + +Linea and Espresso both found high-impact root-of-unity/Lagrange bugs and +zero-inversion issues. + +### 8. Transcript Equivalence + +Differential trace tests should confirm: + +- VK/SRS/circuit digest absorbed before proof messages; +- all public inputs, lengths, and indices absorbed; +- every prover message absorbed before the next challenge; +- curve ID and encoding mode bound where required; +- challenge rounds domain-separated; +- fixed-width encodings are unambiguous; +- no `abi.encodePacked` ambiguity for variable-length data; +- no unreviewed prepend/rehash deviation from the reference transcript; +- failed hash/precompile calls cannot become predictable challenges. + +Use the Rust verifier transcript as the oracle. + +### 9. Halo2/KZG Opening Logic + +PCS tests must isolate: + +- KZG pairing equation sign and ordering; +- whether `A`/opening accumulator is negated; +- SHPLONK accumulator construction; +- multi-opening batching challenges; +- non-empty accumulator/vector checks; +- each evaluation paired with the correct commitment and point; +- rotations: `zeta`, `zeta * omega`, last-row rotations, wraparound; +- lookup and permutation arguments; +- quotient chunk count and degree; +- whether linearization evaluations are proof-provided or recomputed; +- compatibility with the exact prover revision. + +Native PCS deciders accepting empty vectors should have explicit negative +tests. + +### 10. Circuit Shape Coverage + +Generated templates must not bake in the first supported circuit shape. Cover: + +- zero, one, and many custom gate commitments; +- zero, one, and many lookup arguments; +- variable advice/fixed/instance column counts; +- multiple proof systems or aggregation depths; +- invalid empty arrays; +- exact public input count; +- proof layout generated from metadata rather than hand-maintained constants. + +Linea's "one commitment" failure class belongs here. + +### 11. Memory And Yul Rules + +Regression tests should assert: + +- Solidity free memory pointer conventions are respected; +- no generated writes clobber `0x40` or reserved scratch unexpectedly; +- verifier-owned memory starts at `0x80` or a reviewed safe offset; +- generated memory map covers transcript, state, scratch, precompile inputs, + and precompile outputs; +- computed values cannot overwrite `state_success` or other high-value flags; +- failure paths stop early enough and cannot later turn success true; +- debug writes and unused state fields are absent; +- solc version is fixed and checked; +- generated artifacts are reproducible from source. + +Axiom's Halo2 audit and Linea's formal appendix both make memory layout a +first-class verifier invariant. + +### 12. Error Behavior + +Define and test the API policy: + +- malformed calldata -> revert; +- non-canonical field element -> revert; +- invalid curve/subgroup point -> revert; +- precompile failure -> revert; +- wrong proof length -> revert; +- mathematically well-formed false proof -> return `false` or revert, but be + consistent; +- internal invariant violation -> revert. + +General-purpose verifier APIs may return `false` for well-formed invalid +proofs, while protocol entrypoints should usually revert. + +### 13. Malicious-Prover Negative Suite + +Minimum adversarial cases: + +- valid reference proof from the canonical prover; +- every proof byte flipped one at a time; +- every scalar replaced by `x + r`, `r`, `r - 1`, `0`, `1`; +- every Fq coordinate replaced by `x + q`, `q`, `q - 1`, non-canonical + 48-byte values; +- G1/G2 infinity in every commitment slot; +- off-curve points; +- wrong-subgroup BLS points; +- short, long, and trailing-byte proof lengths; +- wrong public input count; +- public input not in Fr; +- wrong endianness; +- wrong VK digest; +- wrong SRS element; +- wrong domain size; +- `zeta` forced to a domain point; +- zero denominators in batch inversion; +- mocked precompile failure; +- pairing precompile returns zero; +- empty accumulator/MSM/vector; +- multiple and zero custom commitments; +- differential tests against the Rust verifier. + +Scroll recommends malicious-prover testing because reference traces mostly +exercise completeness, not soundness. + +### 14. Lightweight Formal Targets + +High-value helpers for formal specs or bounded symbolic tests: + +- `load_fr`, `load_fq`, `load_g1`, `load_g2`; +- `is_canonical_fr`, `is_canonical_fq`; +- `batch_invert`; +- `invert_nonzero`; +- `evaluate_lagrange`; +- `evaluate_pi_poly`; +- `hash_to_field` / transcript challenge derivation; +- precompile wrappers; +- memory allocator and scratch separation; +- `state_success` monotonicity; +- calldata layout precheck; +- VK codehash/deployment precheck; +- pairing equation wrapper. + +Linea used Dafny/Z3 to prove small verifier properties; replicate that style +for compact helpers before trying to prove the whole verifier. + +### 15. BLS-Specific Tests + +BLS-only concerns: + +- Fq is 381 bits, so coordinates cannot fit in Solidity `uint256`; +- Fr fits in 255 bits and can use `addmod`/`mulmod` only after canonical load; +- G1/G2 cofactors require subgroup validation or a documented precompile + guarantee; +- G2 Fq2 coefficient ordering must be locked down with vectors; +- pairing precompile invalid-input semantics must be known; +- MSM precompile behavior for empty MSM, infinity bases, zero scalars, + non-canonical scalars, and subgroup checks must be known; +- compressed proof-point decompression/sign handling should be implemented + once and tested heavily. + +Worldcoin's compressed-proof audit notes belong in this family. + +### 16. Code Generator Invariants + +Generator-level tests should produce and consume: + +- machine-readable proof layout manifest; +- Solidity offsets generated from that manifest; +- tests generated from the same manifest; +- static assertions for proof length, public input count, and VK constants; +- reference JSON vector: proof, public inputs, transcript challenges, expected + pairing inputs; +- negative tests for every field/point input; +- memory map with non-overlap assertions; +- NatSpec and custom errors; +- pinned solc/version metadata in generated output; +- reproducible build artifact and bytecode-hash comparison in CI. + +Worldcoin's generator audit recommendations reinforce the rule: bake template +properties into the generator once, instead of manually patching each verifier. + +### Condensed Do-Not-Ship-Without Tests + +Do not ship without tests for: + +- strict proof length checks; +- canonical Fr/Fq loading; +- explicit G1/G2 on-curve/subgroup/infinity policy; +- every precompile success and return value; +- pairing result value; +- zero inversion rejection; +- Lagrange root-of-unity case; +- VK/SRS/public inputs bound into transcript; +- Solidity memory pointer conventions; +- invalid proof and invalid encoding rejection; +- differential equivalence against the reference Halo2 verifier. diff --git a/proofs/solidity-verifier/docs/reference/TRACE_VARIABLES.md b/proofs/solidity-verifier/docs/reference/TRACE_VARIABLES.md new file mode 100644 index 000000000..37721af8d --- /dev/null +++ b/proofs/solidity-verifier/docs/reference/TRACE_VARIABLES.md @@ -0,0 +1,92 @@ +# Rust/Solidity Trace Variables + +This file documents the differential trace shared by Midfall's native PLONK +verifier and this repo's generated Solidity verifier. The trace is used by the +IVC Keccak test in this repo and by Moonlight's wrap-decider Solidity bench. + +The native side is Midfall's `midnight_proofs::plonk::solidity_trace` module. +The Solidity side is emitted by `templates/contracts/Halo2Verifier.sol` when +rendered with `RenderDiagnostics { trace: true, .. }`. Both sides key events by +a numeric trace ID and compare the raw payload bytes. + +## Event Format + +Each Solidity trace event is a `LOG1`: + +| Payload | Solidity helper | Bytes | +| --- | --- | --- | +| Scalar / u256 | `trace_u256(id, value)` | 32-byte big-endian word | +| BLS12-381 G1 point | `trace_point(id, mptr)` | 128-byte EIP-2537 padded point | +| Variable-length diagnostic bytes | emitted from native trace only or matching Solidity point-set traces | depends on trace range | + +Gas checkpoints are also `LOG1`, but they have empty data and encode +`(checkpoint_id << 248) | gas()` in the topic. Trace parsers skip empty-data +logs so gas checkpoints and differential trace events can coexist. + +## Fixed IDs + +| ID | Native Midfall name | Solidity source | Meaning | +| ---: | --- | --- | --- | +| 1 | `vk_digest` | `mload(VK_DIGEST_MPTR)` | Transcript representation of the verifying key digest. | +| 2 | `num_instances` | generated `num_instances` constant | Number of public instance values in the non-committed instance column. | +| 3 | `k` | generated domain `k` constant | Circuit domain size exponent. | +| 4 | `n_inv` | `mload(N_INV_MPTR)` | Inverse of domain size `n`. | +| 5 | `omega` | `mload(OMEGA_MPTR)` | Domain root of unity. | +| 6 | `omega_inv` | `mload(OMEGA_INV_MPTR)` | Inverse domain root of unity. | +| 7 | `theta` | `mload(THETA_MPTR)` | Lookup compression challenge. | +| 8 | `beta` | `mload(BETA_MPTR)` | Permutation challenge. | +| 9 | `gamma` | `mload(GAMMA_MPTR)` | Permutation challenge. | +| 10 | `y` | `mload(Y_MPTR)` | Quotient/identity batching challenge. | +| 11 | `x` | `mload(X_MPTR)` | Main evaluation challenge. | +| 12 | `trash_challenge` | `mload(TRASH_CHALLENGE_MPTR)` | Trash-argument challenge; emitted only when the circuit has trashcans. | +| 13 | `x1` | `mload(X1_MPTR)` | PCS batching challenge for point-set commitments/evaluations. | +| 14 | `x2` | `mload(X2_MPTR)` | PCS batching challenge for the `f_eval` fold. | +| 15 | `x3` | `mload(X3_MPTR)` | Evaluation point for the committed `f(X)` polynomial; truncated when `truncated-challenges` is enabled. | +| 16 | `x4` | `mload(X4_MPTR)` | Final PCS commitment/evaluation batching challenge. | +| 17 | `x_n` | `mload(X_N_MPTR)` | `x^n`, used by Lagrange and quotient reconstruction. | +| 18 | `x_n_minus_1_inv` | `mload(X_N_MINUS_1_INV_MPTR)` | `(x^n - 1)^-1`. | +| 19 | `l_last` | `mload(L_LAST_MPTR)` | Last-row Lagrange evaluation. | +| 20 | `l_blind` | `mload(L_BLIND_MPTR)` | Sum of blinding-row Lagrange evaluations. | +| 21 | `l_0` | `mload(L_0_MPTR)` | First-row Lagrange evaluation. | +| 22 | `instance_eval` | `mload(INSTANCE_EVAL_MPTR)` | Batched public-instance polynomial evaluation. | +| 23 | `linearization_expected_eval` | `mload(QUOTIENT_EVAL_MPTR)` | Expected linearization evaluation, equal to `-nu_y(x)`. | +| 24 | `linearization_scalars` | `trace_point(24, QUOTIENT_MPTR)` | 128-byte scalar payload shaped like a point: `x^(n-1)`, `1 - x^n`, `0`, `0`. | +| 25 | `f_com` | `trace_point(25, F_COM_MPTR)` | Proof commitment to the PCS accumulator polynomial `f(X)`. | +| 26 | `pi` | `trace_point(26, PI_MPTR)` | KZG opening proof point. | +| 27 | `pairing_lhs` | `trace_point(27, PAIRING_LHS_MPTR)` | Final KZG pairing left input. | +| 28 | `pairing_rhs` | `trace_point(28, PAIRING_RHS_MPTR)` | Final KZG pairing right input. | +| 29 | `acc_lhs` | `trace_point(29, ACC_LHS_MPTR)` | Generated public-accumulator check LHS point; generator-only for Solidity-facing accumulator checks. | +| 30 | `acc_rhs` | `trace_point(30, ACC_RHS_MPTR)` | Generated public-accumulator check RHS point; generator-only for Solidity-facing accumulator checks. | +| 31 | `f_eval` | `mload(F_EVAL_MPTR)` | Reconstructed evaluation of `f(X)` at `x3`. | +| 32 | `v` | `mload(V_MPTR)` | Final folded PCS evaluation scalar. | +| 33 | `final_com` | `trace_point(33, FINAL_COM_MPTR)` | Final folded PCS commitment. | +| 34 | `linearization_commitment` | trace-only MSM into `lin_scratch` | Materialized linearization commitment used to check the scalar/commitment fold. | +| 35 | `final_result` | `success` | Final pairing/precompile success word. | +| 36 | `quotient_numerator` | `-mload(QUOTIENT_EVAL_MPTR) mod Fr` | Fully grouped quotient numerator `nu_y(x)`. | + +## Dynamic Ranges + +| ID range | Native Midfall name | Solidity source | Meaning | +| ---: | --- | --- | --- | +| `1_000 + i` | `user_challenge` | `CHALLENGE_MPTR[i]` | User-phase challenge squeezed after phase-specific advice commitments. | +| `10_000 + i` | `proof_*_commitment` | `proof_commit_trace_id` | G1 proof commitments in transcript-read order: advice, lookup multiplicities, permutation products, lookup helpers/accumulators, trash commitments, quotient limbs, `f_com`, and `pi`. | +| `20_000 + i` | `proof_*_eval` | `proof_eval_trace_id` | Scalar proof evaluations in transcript-read order, including advice, fixed, permutation, lookup, trash, dummy PCS, and q-eval scalars. | +| `30_000 + i` | `quotient_identity_eval` | generated quotient identity trace ID | Raw partially evaluated identity term before the `y` fold. | +| `40_000 + i` | `pcs_q_com` | `trace_point(40_000 + i, ...)` from the PCS emitter | Materialized `q_com` commitment for sorted PCS point set `i`. | +| `41_000 + i` | `pcs_point_set` | serialized point-set trace from the PCS emitter | Concatenated scalar points in sorted PCS point set `i`. | +| `60_000 + i` | `selector_fold` | `SELECTOR_ACC_MPTR[i]` | Folded contribution for simple selector column `i`. | + +`2_000 + i` is reserved in the codegen protocol for PCS query trace IDs, but +the current Midfall/Solidity differential does not emit that range. + +## Comparison Rules + +The trace tests compare every native Midfall event against a Solidity log with +the same ID and identical payload bytes. Solidity may additionally emit IDs 29 +and 30 when the generated verifier checks a public accumulator, because that +check lives outside Midfall's native PLONK verifier. + +The tests also require representative coverage for the challenge IDs, PCS +folds, pairing inputs, final result, `q_com` point-set commitments, serialized +PCS point sets, and selector folds. This keeps the trace from passing after a +large class of accidental instrumentation omissions. diff --git a/proofs/solidity-verifier/examples/ivc_replay.rs b/proofs/solidity-verifier/examples/ivc_replay.rs new file mode 100644 index 000000000..b4d48927d --- /dev/null +++ b/proofs/solidity-verifier/examples/ivc_replay.rs @@ -0,0 +1,137 @@ +//! Replay harness for the IVC Keccak Solidity verifier. +//! +//! Loads the cached +//! target/ivc-keccak-solidity-dump/{Halo2Verifier.sol, +//! Halo2VerifyingKey.sol, +//! Halo2QuotientEvaluator.sol, +//! calldata.bin} +//! produced by the `ivc_keccak_solidity` test, recompiles via solc, +//! deploys on Prague-spec revm, and calls `verifyProof` with the +//! cached calldata. Skips the multi-minute prove step entirely so +//! we can iterate on the rendered Yul (manual edits or codegen +//! changes) in seconds. +//! +//! Usage: +//! +//! ```text +//! GAS_CAP=500000000 cargo run --release \ +//! --features evm,truncated-challenges,fewer-point-sets \ +//! --example ivc_replay +//! ``` +//! +//! `GAS_CAP` defaults to 5_000_000_000 (5 B) so we always see a +//! genuine OutOfGas vs Revert distinction even for the pathological +//! case the original e2e test hits. + +#[cfg(not(all( + feature = "evm", + feature = "truncated-challenges", + feature = "fewer-point-sets", +)))] +fn main() { + eprintln!("ivc_replay requires --features evm,truncated-challenges,fewer-point-sets"); + std::process::exit(1); +} + +#[cfg(all( + feature = "evm", + feature = "truncated-challenges", + feature = "fewer-point-sets", +))] +use halo2_solidity_verifier::{compile_solidity_with_runs, CallOutcome, Evm}; + +#[cfg(all( + feature = "evm", + feature = "truncated-challenges", + feature = "fewer-point-sets", +))] +const SOLC_OPTIMIZE_RUNS: u32 = 1; + +#[cfg(all( + feature = "evm", + feature = "truncated-challenges", + feature = "fewer-point-sets", +))] +fn main() { + let dump_dir = format!( + "{}/target/ivc-keccak-solidity-dump", + env!("CARGO_MANIFEST_DIR") + ); + + let verifier_solidity = std::fs::read_to_string(format!("{dump_dir}/Halo2Verifier.sol")) + .expect("Halo2Verifier.sol not found in dump dir; run the test first"); + let vk_solidity = std::fs::read_to_string(format!("{dump_dir}/Halo2VerifyingKey.sol")) + .expect("Halo2VerifyingKey.sol not found in dump dir; run the test first"); + let quotient_solidity = + std::fs::read_to_string(format!("{dump_dir}/Halo2QuotientEvaluator.sol")) + .expect("Halo2QuotientEvaluator.sol not found in dump dir; run the test first"); + let calldata = + std::fs::read(format!("{dump_dir}/calldata.bin")).expect("calldata.bin not found"); + + let gas_cap: u64 = std::env::var("GAS_CAP") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(5_000_000_000); + + let t0 = std::time::Instant::now(); + let vk_creation_code = compile_solidity_with_runs(&vk_solidity, SOLC_OPTIMIZE_RUNS); + let quotient_creation_code = compile_solidity_with_runs("ient_solidity, SOLC_OPTIMIZE_RUNS); + let verifier_creation_code = compile_solidity_with_runs(&verifier_solidity, SOLC_OPTIMIZE_RUNS); + println!( + "[ivc-replay] solc compile completed in {:.2?} (verifier bytecode = {} bytes, vk bytecode = {} bytes, quotient bytecode = {} bytes)", + t0.elapsed(), + verifier_creation_code.len(), + vk_creation_code.len(), + quotient_creation_code.len() + ); + + let mut evm = Evm::default(); + let vk_address = evm.create(vk_creation_code); + let quotient_address = evm.create(quotient_creation_code); + let verifier_address = + evm.create_with_two_address_args(verifier_creation_code, vk_address, quotient_address); + println!( + "[ivc-replay] deployed (vk = {vk_address:?}, quotient = {quotient_address:?}, verifier = {verifier_address:?})" + ); + println!( + "[ivc-replay] calldata = {} bytes, gas_cap = {gas_cap}", + calldata.len() + ); + + let t0 = std::time::Instant::now(); + match evm.try_call_with_gas(verifier_address, calldata, gas_cap) { + CallOutcome::Success { + gas_used, + output, + logs, + } => { + let expected: Vec = [vec![0u8; 31], vec![1]].concat(); + let ok = output == expected; + println!( + "[ivc-replay] {} (gas_used = {gas_used}, output = 0x{}, {} log(s)) in {:.2?}", + if ok { "PASS" } else { "BAD-OUTPUT" }, + hex::encode(&output), + logs.len(), + t0.elapsed() + ); + for (i, log) in logs.iter().enumerate() { + let topics: Vec = + log.data.topics().iter().map(|t| hex::encode(t.0)).collect(); + println!(" log[{i}] addr={:?} topics={:?}", log.address, topics); + } + } + CallOutcome::Revert { gas_used, output } => { + println!( + "[ivc-replay] REVERT (gas_used = {gas_used}, output = 0x{}) in {:.2?}", + hex::encode(&output), + t0.elapsed() + ); + } + CallOutcome::Halt { gas_used, reason } => { + println!( + "[ivc-replay] HALT (gas_used = {gas_used}, reason = {reason}) in {:.2?}", + t0.elapsed() + ); + } + } +} diff --git a/proofs/solidity-verifier/rust-toolchain.toml b/proofs/solidity-verifier/rust-toolchain.toml new file mode 100644 index 000000000..ff100edcb --- /dev/null +++ b/proofs/solidity-verifier/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.90.0" diff --git a/proofs/solidity-verifier/scripts/check_release_bytecode_sizes.sh b/proofs/solidity-verifier/scripts/check_release_bytecode_sizes.sh new file mode 100755 index 000000000..a6e073da1 --- /dev/null +++ b/proofs/solidity-verifier/scripts/check_release_bytecode_sizes.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SUMMARY="${1:-"$ROOT_DIR/target/ivc-keccak-solidity-dump/contract-sizes.txt"}" +EIP170_MAX_RUNTIME_SIZE="${EIP170_MAX_RUNTIME_SIZE:-24576}" + +[[ -f "$SUMMARY" ]] || { + echo "contract size summary not found: $SUMMARY" >&2 + exit 1 +} + +extract_number() { + local label="$1" + awk -F': ' -v label="$label" '$1 == label { gsub(",", "", $2); print $2; found=1 } END { if (!found) exit 1 }' "$SUMMARY" +} + +extract_hash() { + local label="$1" + awk -F': ' -v label="$label" '$1 == label { print $2; found=1 } END { if (!found) exit 1 }' "$SUMMARY" +} + +check_size() { + local label="$1" + local size + size="$(extract_number "$label")" + if (( size > EIP170_MAX_RUNTIME_SIZE )); then + echo "$label = $size exceeds EIP-170 max $EIP170_MAX_RUNTIME_SIZE" >&2 + exit 1 + fi + echo "[bytecode-size] $label = $size" +} + +check_hash() { + local label="$1" + local expected="$2" + local actual + actual="$(extract_hash "$label")" + if [[ "$actual" != "$expected" ]]; then + echo "$label mismatch: expected $expected, got $actual" >&2 + exit 1 + fi + echo "[bytecode-hash] $label = $actual" +} + +check_size "Halo2Verifier deployed runtime bytes" +check_size "Halo2VerifyingKey deployed runtime bytes" + +check_hash "Halo2Verifier deployed runtime keccak256" "0x37fca91bd3fe16ac462da8ab79b9891b70d8d7a33af62df713f80a76436fa0b0" +check_hash "Halo2VerifyingKey deployed runtime keccak256" "0x92dc96f0c5182608e61ac3b9a257294749f67c1306b516b898f6e2456ccfdfc9" + +echo "[bytecode-size] release bytecode size and hash checks passed" diff --git a/proofs/solidity-verifier/scripts/ensure_srs_assets.sh b/proofs/solidity-verifier/scripts/ensure_srs_assets.sh new file mode 100755 index 000000000..ad401272d --- /dev/null +++ b/proofs/solidity-verifier/scripts/ensure_srs_assets.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SRS_DIR="${SRS_DIR:-"$ROOT_DIR/.srs"}" +SKIP_DOWNLOAD=0 + +while (($#)); do + case "$1" in + --skip-download) + SKIP_DOWNLOAD=1 + shift + ;; + --srs-dir) + [[ $# -ge 2 ]] || { echo "--srs-dir requires DIR" >&2; exit 1; } + SRS_DIR="$2" + shift 2 + ;; + *) + echo "unknown argument: $1" >&2 + exit 1 + ;; + esac +done + +mkdir -p "$SRS_DIR" + +ensure_asset() { + local name="$1" + local url="$2" + local path="$SRS_DIR/$name" + if [[ -s "$path" ]]; then + echo "[srs] using $name" + return + fi + if [[ "$SKIP_DOWNLOAD" -eq 1 ]]; then + echo "[srs] missing $path" >&2 + exit 1 + fi + echo "[srs] downloading $name" + curl -fL --retry 3 --retry-delay 5 -o "$path" "$url" +} + +ensure_asset "bls_filecoin_2p19" "https://midnight-s3-fileshare-dev-eu-west-1.s3.eu-west-1.amazonaws.com/bls_filecoin_2p19" +ensure_asset "midnight-srs-2p19" "https://srs.midnight.network/midnight-srs-2p19" +ensure_asset "midnight-srs-2p20" "https://srs.midnight.network/midnight-srs-2p20" +ensure_asset "midnight-srs-2p22" "https://srs.midnight.network/midnight-srs-2p22" + +echo "[srs] SRS_DIR=$SRS_DIR" diff --git a/proofs/solidity-verifier/scripts/extract_midfall_comments.py b/proofs/solidity-verifier/scripts/extract_midfall_comments.py new file mode 100755 index 000000000..2d0eda2cc --- /dev/null +++ b/proofs/solidity-verifier/scripts/extract_midfall_comments.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +"""Extract Rust comments from the parent Midfall proofs crate. + +The generated corpus is intentionally source-indexed rather than rewritten into +local code. That gives this verifier repo a complete, reviewable copy of the +upstream explanatory comments while letting local inline comments stay focused +on behavior that is actually ported into Solidity/Yul. +""" + +from __future__ import annotations + +import argparse +import dataclasses +import pathlib +import re +import subprocess +import sys +from typing import Iterable + + +DEFAULT_SOURCE = pathlib.Path("../src") +DEFAULT_OUTPUT = pathlib.Path("docs/MIDFALL_PROOFS_COMMENT_CORPUS.md") +COMMENT_RE = re.compile(r"^(?P\s*)//(?P!|/)?(?P.*)$") + + +@dataclasses.dataclass(frozen=True) +class CommentBlock: + path: pathlib.Path + start: int + end: int + kind: str + lines: tuple[str, ...] + + +def git_value(args: list[str], cwd: pathlib.Path) -> str: + try: + return subprocess.check_output( + ["git", *args], + cwd=cwd, + text=True, + stderr=subprocess.DEVNULL, + ).strip() + except (OSError, subprocess.CalledProcessError): + return "unknown" + + +def git_root(path: pathlib.Path) -> pathlib.Path: + value = git_value(["rev-parse", "--show-toplevel"], path) + return pathlib.Path(value) if value != "unknown" else path + + +def comment_kind(marker: str | None) -> str: + if marker == "!": + return "module-doc" + if marker == "/": + return "item-doc" + return "line" + + +def clean_body(body: str) -> str: + return body[1:] if body.startswith(" ") else body + + +def extract_file(path: pathlib.Path, rel: pathlib.Path) -> list[CommentBlock]: + blocks: list[CommentBlock] = [] + active_start: int | None = None + active_end = 0 + active_kind: str | None = None + active_lines: list[str] = [] + + def flush() -> None: + nonlocal active_start, active_end, active_kind, active_lines + if active_start is not None and active_kind is not None: + blocks.append( + CommentBlock( + path=rel, + start=active_start, + end=active_end, + kind=active_kind, + lines=tuple(active_lines), + ) + ) + active_start = None + active_end = 0 + active_kind = None + active_lines = [] + + for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): + match = COMMENT_RE.match(line) + if match is None: + flush() + continue + + kind = comment_kind(match.group("marker")) + body = clean_body(match.group("body")) + if active_start is None: + active_start = lineno + active_kind = kind + active_lines = [body] + elif active_kind == kind and active_end + 1 == lineno: + active_lines.append(body) + else: + flush() + active_start = lineno + active_kind = kind + active_lines = [body] + active_end = lineno + + flush() + return blocks + + +def source_files(source: pathlib.Path) -> list[pathlib.Path]: + return sorted(source.resolve().rglob("*.rs")) + + +def extract_comments(source: pathlib.Path) -> list[CommentBlock]: + source = source.resolve() + blocks: list[CommentBlock] = [] + for path in source_files(source): + rel = path.relative_to(source) + blocks.extend(extract_file(path, rel)) + return blocks + + +def fence_for(lines: Iterable[str]) -> str: + max_ticks = 2 + for line in lines: + for match in re.finditer(r"`+", line): + max_ticks = max(max_ticks, len(match.group(0))) + return "`" * (max_ticks + 1) + + +def render(source: pathlib.Path, blocks: list[CommentBlock]) -> str: + source = source.resolve() + root = git_root(source) + commit = git_value(["rev-parse", "HEAD"], root) + status = git_value(["status", "--short"], root) + total_file_count = len(source_files(source)) + file_count = len({block.path for block in blocks}) + line_count = sum(len(block.lines) for block in blocks) + + out: list[str] = [ + "# Midfall Proofs Comment Corpus", + "", + "Generated by `scripts/extract_midfall_comments.py` from upstream Rust comments.", + "This file preserves the source comments verbatim by path and line span;", + "verifier-relevant material is adapted inline in this repository's source", + "and templates.", + "", + "## Source", + "", + f"- Source root: `{source}`", + f"- Git root: `{root}`", + f"- Git commit: `{commit}`", + f"- Git status at extraction: `{status or 'clean'}`", + f"- Rust source files: `{total_file_count}`", + f"- Rust files with comments: `{file_count}`", + f"- Comment blocks: `{len(blocks)}`", + f"- Comment lines: `{line_count}`", + "", + "## License And Attribution", + "", + "The comments below originate from the sibling Midfall `proofs` crate.", + "That crate ships Apache-2.0 and MIT license files, and some LogUp", + "files carry explicit Apache-2.0 SPDX/license headers. Preserve those", + "notices when copying or adapting comments into source files.", + "", + ] + + current_path: pathlib.Path | None = None + for block in blocks: + if current_path != block.path: + current_path = block.path + out.extend(["", f"## `{block.path}`", ""]) + + span = f"L{block.start}" if block.start == block.end else f"L{block.start}-L{block.end}" + out.extend([f"### `{block.path}:{span}` ({block.kind})", ""]) + fence = fence_for(block.lines) + out.append(f"{fence}text") + out.extend(block.lines) + out.append(fence) + out.append("") + + return "\n".join(out).rstrip() + "\n" + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source", type=pathlib.Path, default=DEFAULT_SOURCE) + parser.add_argument("--output", type=pathlib.Path, default=DEFAULT_OUTPUT) + parser.add_argument("--check", action="store_true", help="fail if output differs") + parser.add_argument("--write", action="store_true", help="write the generated corpus") + args = parser.parse_args() + + if args.check == args.write: + parser.error("choose exactly one of --check or --write") + + source = args.source.resolve() + if not source.is_dir(): + print(f"source directory not found: {source}", file=sys.stderr) + return 2 + + rendered = render(source, extract_comments(source)) + output = args.output.resolve() + + if args.write: + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(rendered, encoding="utf-8") + print(f"wrote {output}") + return 0 + + current = output.read_text(encoding="utf-8") if output.exists() else "" + if current != rendered: + print( + f"{output} is stale; run `scripts/extract_midfall_comments.py --write`", + file=sys.stderr, + ) + return 1 + + print(f"{output} is up to date") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/proofs/solidity-verifier/scripts/install_pinned_solc.sh b/proofs/solidity-verifier/scripts/install_pinned_solc.sh new file mode 100755 index 000000000..750092dc5 --- /dev/null +++ b/proofs/solidity-verifier/scripts/install_pinned_solc.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +PINNED_SOLC_VERSION="${PINNED_SOLC_VERSION:-0.8.30+commit.73712a01}" +INSTALL_DIR="${1:-${SOLC_INSTALL_DIR:-$PWD/.solc}}" + +case "$(uname -s)-$(uname -m)" in + Linux-x86_64) + platform="linux-amd64" + binary="solc-linux-amd64-v${PINNED_SOLC_VERSION}" + ;; + Darwin-x86_64) + platform="macosx-amd64" + binary="solc-macosx-amd64-v${PINNED_SOLC_VERSION}" + ;; + Darwin-arm64) + platform="macosx-amd64" + binary="solc-macosx-amd64-v${PINNED_SOLC_VERSION}" + ;; + *) + echo "unsupported platform for pinned solc install: $(uname -s)-$(uname -m)" >&2 + exit 1 + ;; +esac + +mkdir -p "$INSTALL_DIR" +solc_path="$INSTALL_DIR/solc" + +if [[ ! -x "$solc_path" ]] || ! "$solc_path" --version | grep -q "Version: ${PINNED_SOLC_VERSION}"; then + url="https://binaries.soliditylang.org/${platform}/${binary}" + echo "[install-solc] downloading $url" + curl -fsSL "$url" -o "$solc_path" + chmod +x "$solc_path" +fi + +"$solc_path" --version +echo "$solc_path" diff --git a/proofs/solidity-verifier/scripts/run_ivc_bench.sh b/proofs/solidity-verifier/scripts/run_ivc_bench.sh new file mode 100755 index 000000000..a11a49e2c --- /dev/null +++ b/proofs/solidity-verifier/scripts/run_ivc_bench.sh @@ -0,0 +1,366 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +CHECK_ONLY=0 +GAS_CHECKPOINTS=1 +RUN_NATIVE_MIDFALL=0 +RUN_SOLIDITY_BENCH=1 +SKIP_SRS_DOWNLOAD=0 +RUN_TRACE=0 +IN_CIRCUIT_FEWER_POINT_SETS=1 +OUTER_SINGLE_H_COMMITMENT=0 +ALLOW_UNPINNED_SOLC="${HALO2_SOLIDITY_ALLOW_UNPINNED_SOLC:-0}" +RUST_TOOLCHAIN="${RUSTUP_TOOLCHAIN:-}" + +SRS_DIR="${SRS_DIR:-"$ROOT_DIR/../../zk_stdlib/examples/assets"}" +MIDFALL_DIR="${MIDFALL_DIR:-"$ROOT_DIR/../.."}" +IVC_LEAF_BUNDLE="${IVC_LEAF_BUNDLE:-"$ROOT_DIR/target/ivc-keccak-solidity-dump/ivc-leaf-bundle.bin"}" + +FILECOIN_SRS_URL="https://midnight-s3-fileshare-dev-eu-west-1.s3.eu-west-1.amazonaws.com/bls_filecoin_2p19" +MIDNIGHT_SRS_2P19_URL="https://srs.midnight.network/midnight-srs-2p19" +MIDNIGHT_SRS_2P20_URL="https://srs.midnight.network/midnight-srs-2p20" +MIDNIGHT_SRS_2P22_URL="https://srs.midnight.network/midnight-srs-2p22" +PINNED_SOLC_VERSION="0.8.30+commit.73712a01" + +usage() { + cat <<'USAGE' +Run the IVC Keccak Solidity verifier bench over Poseidon hash-chain leaves. + +Usage: + scripts/run_ivc_bench.sh [options] + +Options: + --check-only Download/check SRS assets and compile the ignored tests + without running the slow proving/verifier bench. + --skip-srs-download Fail if a required SRS asset is missing. + --srs-dir DIR Directory for SRS assets. Defaults to $SRS_DIR or + ../../zk_stdlib/examples/assets. + --no-gas-checkpoints Run the Solidity verifier bench without section logs. + --outer-single-h-commitment + Enable the single-H quotient commitment layout + for the final Solidity-facing decider proof. + --leaf-bundle PATH Path for the generated/reused multi-limb IVC leaf + bundle. Defaults to target/ivc-keccak-solidity-dump/ + ivc-leaf-bundle.bin. + --trace Enable native Rust/Solidity trace equivalence. + Requires midnight-proofs/solidity-verifier-trace. + --allow-unpinned-solc + Use the configured solc even if it does not match the + pinned reproducible compiler. Gas/bytecode are ad hoc. + --rust-toolchain NAME + Export RUSTUP_TOOLCHAIN=NAME for cargo commands + (for example, stable) instead of rustup's default + rust-toolchain.toml selection. + --native-midfall Also run Midfall's native Poseidon-chain final IVC test from + $MIDFALL_DIR/aggregation. + --native-only Run only the Midfall native Poseidon-chain final IVC test. + --midfall-dir DIR Local Midfall checkout for --native-midfall. + Defaults to this repository root. + -h, --help Show this help. + +Default behavior: + 1. Ensure SRS_DIR has Midnight's midnight-srs-2p19 and midnight-srs-2p20. + The optional outer single-H path also needs midnight-srs-2p22. + The optional --native-midfall path also needs a Filecoin SRS. + 2. Compile the gated Solidity verifier bench. + 3. If --outer-single-h-commitment is set, generate the multi-limb IVC leaf + bundle without outer-single-h-commitment, then run the final decider + Solidity bench with outer-single-h-commitment. + +Examples: + scripts/run_ivc_bench.sh --check-only + scripts/run_ivc_bench.sh + scripts/run_ivc_bench.sh --trace --no-gas-checkpoints + scripts/run_ivc_bench.sh --allow-unpinned-solc --rust-toolchain stable + SRS_DIR=/path/to/srs scripts/run_ivc_bench.sh --native-midfall +USAGE +} + +die() { + echo "[ivc-bench] error: $*" >&2 + exit 1 +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || die "required command not found on PATH: $1" +} + +env_flag_enabled() { + case "${1:-}" in + 1|true|TRUE|yes|YES|on|ON) return 0 ;; + *) return 1 ;; + esac +} + +require_pinned_solc() { + local solc_bin="${SOLC:-solc}" + command -v "$solc_bin" >/dev/null 2>&1 || die "required command not found: $solc_bin" + local actual + actual="$("$solc_bin" --version | awk '/^Version: / { print $2; exit }')" + if env_flag_enabled "$ALLOW_UNPINNED_SOLC"; then + echo "[ivc-bench] warning: using unpinned solc $actual; gas/bytecode are not reproducible against pinned $PINNED_SOLC_VERSION" + return + fi + [[ "$actual" == "$PINNED_SOLC_VERSION"* ]] || die "solc version $actual does not match pinned $PINNED_SOLC_VERSION" +} + +abs_path() { + case "$1" in + /*) printf '%s\n' "$1" ;; + *) printf '%s\n' "$PWD/${1#./}" ;; + esac +} + +while (($#)); do + case "$1" in + --check-only) + CHECK_ONLY=1 + ;; + --skip-srs-download) + SKIP_SRS_DOWNLOAD=1 + ;; + --srs-dir) + [[ $# -ge 2 ]] || die "--srs-dir requires a value" + SRS_DIR="$2" + shift + ;; + --no-gas-checkpoints) + GAS_CHECKPOINTS=0 + ;; + --outer-single-h-commitment) + OUTER_SINGLE_H_COMMITMENT=1 + ;; + --leaf-bundle) + [[ $# -ge 2 ]] || die "--leaf-bundle requires a value" + IVC_LEAF_BUNDLE="$2" + shift + ;; + --trace) + RUN_TRACE=1 + ;; + --allow-unpinned-solc) + ALLOW_UNPINNED_SOLC=1 + ;; + --rust-toolchain) + [[ $# -ge 2 ]] || die "--rust-toolchain requires a value" + RUST_TOOLCHAIN="$2" + shift + ;; + --native-midfall) + RUN_NATIVE_MIDFALL=1 + ;; + --native-only) + RUN_NATIVE_MIDFALL=1 + RUN_SOLIDITY_BENCH=0 + ;; + --midfall-dir) + [[ $# -ge 2 ]] || die "--midfall-dir requires a value" + MIDFALL_DIR="$2" + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "unknown option: $1" + ;; + esac + shift +done + +SRS_DIR="$(abs_path "$SRS_DIR")" +MIDFALL_DIR="$(abs_path "$MIDFALL_DIR")" +IVC_LEAF_BUNDLE="$(abs_path "$IVC_LEAF_BUNDLE")" + +if env_flag_enabled "$ALLOW_UNPINNED_SOLC"; then + export HALO2_SOLIDITY_ALLOW_UNPINNED_SOLC=1 +fi +if [[ -n "$RUST_TOOLCHAIN" ]]; then + export RUSTUP_TOOLCHAIN="$RUST_TOOLCHAIN" +fi + +download_if_missing() { + local path="$1" + local url="$2" + + if [[ -s "$path" ]]; then + echo "[ivc-bench] using $(basename "$path")" + return + fi + + [[ "$SKIP_SRS_DOWNLOAD" -eq 0 ]] || die "missing SRS asset: $path" + require_cmd curl + + mkdir -p "$(dirname "$path")" + local tmp="$path.partial" + echo "[ivc-bench] downloading $(basename "$path")" + echo "[ivc-bench] from $url" + curl -fL --retry 3 --retry-delay 2 -o "$tmp" "$url" + mv "$tmp" "$path" +} + +ensure_srs_assets() { + mkdir -p "$SRS_DIR" + + download_if_missing "$SRS_DIR/midnight-srs-2p19" "$MIDNIGHT_SRS_2P19_URL" + download_if_missing "$SRS_DIR/midnight-srs-2p20" "$MIDNIGHT_SRS_2P20_URL" + if [[ "$OUTER_SINGLE_H_COMMITMENT" -eq 1 ]]; then + download_if_missing "$SRS_DIR/midnight-srs-2p22" "$MIDNIGHT_SRS_2P22_URL" + fi +} + +ensure_filecoin_srs_asset() { + if [[ -s "$SRS_DIR/bls_filecoin_2p13" ]]; then + echo "[ivc-bench] using bls_filecoin_2p13" + elif [[ -s "$SRS_DIR/bls_filecoin_2p19" ]]; then + echo "[ivc-bench] using bls_filecoin_2p19; Midfall will downsize to bls_filecoin_2p13 if needed" + else + download_if_missing "$SRS_DIR/bls_filecoin_2p19" "$FILECOIN_SRS_URL" + fi +} + +cargo_features() { + local include_outer_single_h="${1:-$OUTER_SINGLE_H_COMMITMENT}" + local features="evm,truncated-challenges" + if [[ "$IN_CIRCUIT_FEWER_POINT_SETS" -eq 1 ]]; then + features="$features,in-circuit-fewer-point-sets" + fi + if [[ "$include_outer_single_h" -eq 1 ]]; then + features="$features,outer-single-h-commitment" + fi + if [[ "$GAS_CHECKPOINTS" -eq 1 ]]; then + features="$features,solidity-gas-checkpoints" + fi + if [[ "$RUN_TRACE" -eq 1 ]]; then + features="$features,solidity-trace" + fi + if [[ "$RUN_TRACE" -eq 1 ]]; then + features="$features,rust-verifier-trace" + fi + printf '%s\n' "$features" +} + +run_native_midfall() { + [[ -f "$MIDFALL_DIR/aggregation/Cargo.toml" ]] || die "Midfall aggregation Cargo.toml not found under $MIDFALL_DIR" + ensure_filecoin_srs_asset + + echo "[ivc-bench] compiling Midfall native Poseidon-chain final IVC test" + echo "+ SRS_DIR=$SRS_DIR cargo test --release --manifest-path $MIDFALL_DIR/aggregation/Cargo.toml --test ivc_keccak_final --features keccak-transcript,truncated-challenges,fewer-point-sets --no-run" + SRS_DIR="$SRS_DIR" cargo test --release \ + --manifest-path "$MIDFALL_DIR/aggregation/Cargo.toml" \ + --test ivc_keccak_final \ + --features keccak-transcript,truncated-challenges,fewer-point-sets \ + --no-run + + if [[ "$CHECK_ONLY" -eq 0 ]]; then + echo "[ivc-bench] running Midfall native Poseidon-chain final IVC test" + echo "+ SRS_DIR=$SRS_DIR cargo test --release --manifest-path $MIDFALL_DIR/aggregation/Cargo.toml --test ivc_keccak_final --features keccak-transcript,truncated-challenges,fewer-point-sets -- --ignored --nocapture" + SRS_DIR="$SRS_DIR" cargo test --release \ + --manifest-path "$MIDFALL_DIR/aggregation/Cargo.toml" \ + --test ivc_keccak_final \ + --features keccak-transcript,truncated-challenges,fewer-point-sets \ + -- --ignored --nocapture + fi +} + +run_solidity_bench() { + local features + local leaf_features + features="$(cargo_features "$OUTER_SINGLE_H_COMMITMENT")" + leaf_features="$(cargo_features 0)" + + if [[ "$CHECK_ONLY" -eq 0 ]]; then + require_pinned_solc + fi + + if [[ "$OUTER_SINGLE_H_COMMITMENT" -eq 1 ]]; then + echo "[ivc-bench] compiling multi-limb IVC leaf-bundle generator" + echo "+ SRS_DIR=$SRS_DIR cargo test --release --features $leaf_features --test ivc_keccak_solidity ivc_final_keccak_solidity_e2e --no-run" + ( + cd "$ROOT_DIR" + SRS_DIR="$SRS_DIR" cargo test --release \ + --features "$leaf_features" \ + --test ivc_keccak_solidity ivc_final_keccak_solidity_e2e \ + --no-run + ) + + if [[ "$CHECK_ONLY" -eq 0 ]]; then + mkdir -p "$(dirname "$IVC_LEAF_BUNDLE")" + echo "[ivc-bench] generating multi-limb IVC leaf bundle" + echo "+ HALO2_SOLIDITY_RUN_IVC_BENCH=1 HALO2_SOLIDITY_WRITE_IVC_LEAF_BUNDLE=1 HALO2_SOLIDITY_IVC_LEAF_BUNDLE=$IVC_LEAF_BUNDLE SRS_DIR=$SRS_DIR cargo test --release --features $leaf_features --test ivc_keccak_solidity ivc_final_keccak_solidity_e2e -- --nocapture" + ( + cd "$ROOT_DIR" + HALO2_SOLIDITY_RUN_IVC_BENCH=1 \ + HALO2_SOLIDITY_WRITE_IVC_LEAF_BUNDLE=1 \ + HALO2_SOLIDITY_IVC_LEAF_BUNDLE="$IVC_LEAF_BUNDLE" \ + SRS_DIR="$SRS_DIR" cargo test --release \ + --features "$leaf_features" \ + --test ivc_keccak_solidity ivc_final_keccak_solidity_e2e \ + -- --nocapture + ) + fi + fi + + echo "[ivc-bench] compiling IVC Keccak Solidity verifier bench (Poseidon-chain leaves)" + echo "+ SRS_DIR=$SRS_DIR cargo test --release --features $features --test ivc_keccak_solidity ivc_final_keccak_solidity_e2e --no-run" + ( + cd "$ROOT_DIR" + SRS_DIR="$SRS_DIR" cargo test --release \ + --features "$features" \ + --test ivc_keccak_solidity ivc_final_keccak_solidity_e2e \ + --no-run + ) + + if [[ "$CHECK_ONLY" -eq 0 ]]; then + echo "[ivc-bench] running IVC Keccak Solidity verifier bench (Poseidon-chain leaves)" + echo "+ HALO2_SOLIDITY_RUN_IVC_BENCH=1 HALO2_SOLIDITY_IVC_LEAF_BUNDLE=$IVC_LEAF_BUNDLE SRS_DIR=$SRS_DIR cargo test --release --features $features --test ivc_keccak_solidity ivc_final_keccak_solidity_e2e -- --nocapture" + ( + cd "$ROOT_DIR" + HALO2_SOLIDITY_RUN_IVC_BENCH=1 \ + HALO2_SOLIDITY_IVC_LEAF_BUNDLE="$IVC_LEAF_BUNDLE" \ + SRS_DIR="$SRS_DIR" cargo test --release \ + --features "$features" \ + --test ivc_keccak_solidity ivc_final_keccak_solidity_e2e \ + -- --nocapture + ) + + local dump_dir="$ROOT_DIR/target/ivc-keccak-solidity-dump" + if [[ -d "$dump_dir" ]]; then + echo "[ivc-bench] generated Solidity artifacts: $dump_dir" + if [[ -f "$dump_dir/contract-sizes.txt" ]]; then + echo "[ivc-bench] contract sizes:" + cat "$dump_dir/contract-sizes.txt" + fi + fi + fi +} + +require_cmd cargo +ensure_srs_assets + +echo "[ivc-bench] SRS_DIR=$SRS_DIR" +if env_flag_enabled "$ALLOW_UNPINNED_SOLC"; then + echo "[ivc-bench] unpinned solc: enabled" +fi +if [[ -n "${RUSTUP_TOOLCHAIN:-}" ]]; then + echo "[ivc-bench] RUSTUP_TOOLCHAIN=$RUSTUP_TOOLCHAIN" +fi +if [[ "$OUTER_SINGLE_H_COMMITMENT" -eq 1 ]]; then + echo "[ivc-bench] outer single-H commitment: enabled" + echo "[ivc-bench] IVC_LEAF_BUNDLE=$IVC_LEAF_BUNDLE" +else + echo "[ivc-bench] outer single-H commitment: disabled" +fi + +if [[ "$RUN_NATIVE_MIDFALL" -eq 1 ]]; then + run_native_midfall +fi + +if [[ "$RUN_SOLIDITY_BENCH" -eq 1 ]]; then + run_solidity_bench +fi + +echo "[ivc-bench] done" diff --git a/proofs/solidity-verifier/scripts/run_team_demo.sh b/proofs/solidity-verifier/scripts/run_team_demo.sh new file mode 100755 index 000000000..9e3e44f7b --- /dev/null +++ b/proofs/solidity-verifier/scripts/run_team_demo.sh @@ -0,0 +1,587 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +REPO_NAME="$(basename "$ROOT_DIR")" +cd "$ROOT_DIR" + +MIDFALL_DIR="$(cd "$ROOT_DIR/../.." && pwd)" +if [[ -f "$MIDFALL_DIR/Cargo.toml" && -d "$MIDFALL_DIR/aggregation" && -d "$MIDFALL_DIR/curves" ]]; then + DEFAULT_MOONLIGHT_DIR="$(cd "$MIDFALL_DIR/.." && pwd)/Moonlight" +else + DEFAULT_MOONLIGHT_DIR="$ROOT_DIR/../Moonlight" +fi + +PINNED_SOLC_VERSION="${PINNED_SOLC_VERSION:-0.8.30+commit.73712a01}" +SOLC_INSTALL_DIR="${SOLC_INSTALL_DIR:-"$ROOT_DIR/.solc"}" +SRS_DIR="${SRS_DIR:-"$ROOT_DIR/.srs"}" +LOG_DIR="${LOG_DIR:-"$ROOT_DIR/target/team-demo-logs"}" + +MOONLIGHT_DIR="${MOONLIGHT_DIR:-"$DEFAULT_MOONLIGHT_DIR"}" +MOONLIGHT_BRANCH="${MOONLIGHT_BRANCH:-codex/wrap-bench-cherry-picks}" +MOONLIGHT_URL="${MOONLIGHT_URL:-git@github.com:EYBlockchain/Moonlight.git}" +MOONLIGHT_HTTPS_URL="${MOONLIGHT_HTTPS_URL:-https://github.com/EYBlockchain/Moonlight.git}" +MOONLIGHT_WRAP_TEST="wrap_circuit_composes_two_fold_children_from_four_dummy_fold_proofs" +MOONLIGHT_DUMP_DIR="${MOONLIGHT_WRAP_SOLIDITY_DUMP_DIR:-"$ROOT_DIR/target/moonlight-wrap-solidity-dump"}" + +CHECK_ONLY=0 +SKIP_SRS_DOWNLOAD=0 +RUN_TRACE=1 +RUN_MOONLIGHT=1 +CLONE_MOONLIGHT=1 +UPDATE_MOONLIGHT=0 +USE_HTTPS=0 +INSTALL_SOLC=1 +PRECHECK_BUILDS=1 +FIX_MOONLIGHT_DEP=1 +ALLOW_UNPINNED_SOLC="${HALO2_SOLIDITY_ALLOW_UNPINNED_SOLC:-0}" +RUST_TOOLCHAIN="${RUSTUP_TOOLCHAIN:-}" + +if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then + BOLD=$'\033[1m' + DIM=$'\033[2m' + RED=$'\033[31m' + GREEN=$'\033[32m' + YELLOW=$'\033[33m' + BLUE=$'\033[34m' + MAGENTA=$'\033[35m' + CYAN=$'\033[36m' + RESET=$'\033[0m' +else + BOLD="" + DIM="" + RED="" + GREEN="" + YELLOW="" + BLUE="" + MAGENTA="" + CYAN="" + RESET="" +fi + +usage() { + cat <&2 + exit 1 +} + +warn() { + printf '%s[demo:warn]%s %s\n' "$YELLOW" "$RESET" "$*" +} + +info() { + printf '%s[demo]%s %s\n' "$BLUE" "$RESET" "$*" +} + +ok() { + printf '%s[demo:ok]%s %s\n' "$GREEN" "$RESET" "$*" +} + +section() { + printf '\n%s%s%s\n' "$BOLD$CYAN" "$*" "$RESET" +} + +explain() { + printf '%s%s%s\n' "$DIM" "$*" "$RESET" +} + +quote_cmd() { + local quoted=() + local arg + for arg in "$@"; do + printf -v arg '%q' "$arg" + quoted+=("$arg") + done + printf '%s' "${quoted[*]}" +} + +slugify() { + printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-//; s/-$//' +} + +moonlight_manifest_path() { + printf '%s/aggregation/Cargo.toml' "$MOONLIGHT_DIR" +} + +print_moonlight_gas_command() { + [[ "$RUN_MOONLIGHT" -eq 1 ]] || return 0 + + printf '%s\n' "Moonlight bench + trace command:" + printf '%s\n' " MOONLIGHT_RUN_WRAP_SOLIDITY_BENCH=1 \\" + printf '%s\n' " MOONLIGHT_RUN_WRAP_SOLIDITY_TRACE=1 \\" + printf '%s\n' " cargo test --manifest-path $(moonlight_manifest_path) \\" + printf '%s\n' " $MOONLIGHT_WRAP_TEST --release \\" + printf '%s\n' " --lib -- --ignored --nocapture" +} + +run_logged() { + local label="$1" + shift + + mkdir -p "$LOG_DIR" + local slug + slug="$(slugify "$label")" + local stamp + stamp="$(date +"%Y%m%d-%H%M%S")" + local log_file="$LOG_DIR/${stamp}-${slug}.log" + + section "$label" + explain "Log: $log_file" + printf '%s+ %s%s\n' "$MAGENTA" "$(quote_cmd "$@")" "$RESET" + + local start + start="$(date +%s)" + if "$@" 2>&1 | tee "$log_file"; then + local end + end="$(date +%s)" + ok "$label completed in $((end - start))s" + else + local status=$? + warn "$label failed. Full log: $log_file" + return "$status" + fi +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || die "required command not found on PATH: $1" +} + +env_flag_enabled() { + case "${1:-}" in + 1|true|TRUE|yes|YES|on|ON) return 0 ;; + *) return 1 ;; + esac +} + +abs_path() { + case "$1" in + /*) printf '%s\n' "$1" ;; + *) printf '%s\n' "$PWD/${1#./}" ;; + esac +} + +dependency_abs_path() { + local base_dir="$1" + local dep_path="$2" + local parent + local leaf + + case "$dep_path" in + /*) + parent="$(dirname "$dep_path")" + leaf="$(basename "$dep_path")" + ;; + *) + parent="$base_dir/$(dirname "$dep_path")" + leaf="$(basename "$dep_path")" + ;; + esac + + local parent_abs + parent_abs="$(cd "$parent" 2>/dev/null && pwd -P)" || return 1 + printf '%s/%s\n' "$parent_abs" "$leaf" +} + +solc_version() { + local solc_bin="$1" + "$solc_bin" --version 2>/dev/null | awk '/^Version: / { print $2; exit }' +} + +ensure_solc() { + if env_flag_enabled "$ALLOW_UNPINNED_SOLC"; then + section "Solidity Compiler" + explain "Unpinned solc is enabled; generated bytecode and gas are not reproducible." + + local solc_bin="${SOLC:-solc}" + require_cmd "$solc_bin" + export SOLC="$solc_bin" + export HALO2_SOLIDITY_ALLOW_UNPINNED_SOLC=1 + warn "Using unpinned solc: $SOLC ($(solc_version "$SOLC"))" + return + fi + + section "Pinned Solidity Compiler" + explain "The generated bytecode is reproducible only with solc $PINNED_SOLC_VERSION." + + local preferred="${SOLC:-"$SOLC_INSTALL_DIR/solc"}" + if [[ -x "$preferred" && "$(solc_version "$preferred")" == "$PINNED_SOLC_VERSION"* ]]; then + export SOLC="$preferred" + ok "Using pinned solc: $SOLC" + "$SOLC" --version + return + fi + + if [[ "$INSTALL_SOLC" -eq 0 ]]; then + die "pinned solc not found. Set SOLC or rerun without --no-solc-install." + fi + + require_cmd curl + run_logged "Install pinned solc" "$ROOT_DIR/scripts/install_pinned_solc.sh" "$SOLC_INSTALL_DIR" + export SOLC="$SOLC_INSTALL_DIR/solc" + [[ -x "$SOLC" ]] || die "solc install did not create $SOLC" + [[ "$(solc_version "$SOLC")" == "$PINNED_SOLC_VERSION"* ]] || die "installed solc is not pinned $PINNED_SOLC_VERSION" + ok "SOLC=$SOLC" +} + +check_disk_space() { + local available_kb + available_kb="$(df -Pk "$ROOT_DIR" | awk 'NR == 2 { print $4 }')" + local available_gb=$((available_kb / 1024 / 1024)) + if (( available_gb < 20 )); then + warn "Only ${available_gb}GB free under $ROOT_DIR. The full demo is happier with 20GB+." + else + ok "Disk space: ${available_gb}GB free near repo" + fi +} + +check_tools() { + section "Toolchain Checks" + explain "These checks catch the boring setup problems before we start proving." + + require_cmd git + require_cmd cargo + require_cmd rustc + require_cmd sed + require_cmd awk + require_cmd tee + + ok "git: $(git --version)" + ok "cargo: $(cargo --version)" + ok "rustc: $(rustc --version)" + if command -v rustup >/dev/null 2>&1; then + ok "rustup active toolchain: $(rustup show active-toolchain 2>/dev/null || true)" + else + warn "rustup is not on PATH. Cargo may still work if the pinned toolchain is already active." + fi + check_disk_space +} + +setup_moonlight() { + section "Moonlight Checkout" + explain "The wrap bench lives in Moonlight, while its dev dependency points back to this verifier checkout." + + local clone_url="$MOONLIGHT_URL" + if [[ "$USE_HTTPS" -eq 1 ]]; then + clone_url="$MOONLIGHT_HTTPS_URL" + fi + + if [[ ! -d "$MOONLIGHT_DIR/.git" ]]; then + [[ "$CLONE_MOONLIGHT" -eq 1 ]] || die "Moonlight checkout missing at $MOONLIGHT_DIR" + mkdir -p "$(dirname "$MOONLIGHT_DIR")" + run_logged "Clone Moonlight" git clone "$clone_url" "$MOONLIGHT_DIR" + UPDATE_MOONLIGHT=1 + else + ok "Moonlight checkout: $MOONLIGHT_DIR" + fi + + local current_branch + current_branch="$(git -C "$MOONLIGHT_DIR" branch --show-current || true)" + if [[ "$UPDATE_MOONLIGHT" -eq 1 || "$current_branch" != "$MOONLIGHT_BRANCH" ]]; then + local tracked_changes + tracked_changes="$(git -C "$MOONLIGHT_DIR" status --porcelain --untracked-files=no)" + [[ -z "$tracked_changes" ]] || die "Moonlight has tracked local changes. Commit/stash them before switching branches." + + run_logged "Fetch Moonlight branch" git -C "$MOONLIGHT_DIR" fetch origin "$MOONLIGHT_BRANCH" + if git -C "$MOONLIGHT_DIR" show-ref --verify --quiet "refs/heads/$MOONLIGHT_BRANCH"; then + run_logged "Switch Moonlight branch" git -C "$MOONLIGHT_DIR" switch "$MOONLIGHT_BRANCH" + else + run_logged "Create Moonlight branch" git -C "$MOONLIGHT_DIR" switch -c "$MOONLIGHT_BRANCH" --track "origin/$MOONLIGHT_BRANCH" + fi + fi + + current_branch="$(git -C "$MOONLIGHT_DIR" branch --show-current || true)" + ok "Moonlight branch: $current_branch" + + local manifest + manifest="$(moonlight_manifest_path)" + [[ -f "$manifest" ]] || die "Moonlight manifest not found: $manifest" + + local manifest_dir + manifest_dir="$(dirname "$manifest")" + local dep_path + dep_path="$( + sed -nE '/halo2_solidity_verifier[[:space:]]*=/ { + s/.*path[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p + q + }' "$manifest" + )" + [[ -n "$dep_path" ]] || die "Moonlight manifest has no halo2_solidity_verifier path dependency: $manifest" + + local dep_abs + dep_abs="$(dependency_abs_path "$manifest_dir" "$dep_path")" || die "Could not resolve Moonlight verifier dependency path \"$dep_path\" from $manifest_dir" + + if [[ ! -f "$dep_abs/Cargo.toml" ]]; then + if [[ -e "$dep_abs" || -L "$dep_abs" ]]; then + die "Moonlight verifier dependency exists but has no Cargo.toml: $dep_abs" + fi + if [[ "$FIX_MOONLIGHT_DEP" -eq 0 ]]; then + die "Moonlight verifier dependency is missing: $dep_abs. Rerun without --no-fix-moonlight-dep or update $manifest." + fi + + warn "Moonlight expects halo2_solidity_verifier at $dep_abs, but that path is missing." + warn "Creating a local symlink so Moonlight resolves to this checkout: $ROOT_DIR" + ln -s "$ROOT_DIR" "$dep_abs" + fi + + local root_physical + local dep_physical + root_physical="$(cd "$ROOT_DIR" && pwd -P)" + dep_physical="$(cd "$dep_abs" && pwd -P)" + if [[ "$dep_physical" == "$root_physical" ]]; then + ok "Moonlight dev dependency path \"$dep_path\" resolves to this checkout" + else + die "Moonlight halo2_solidity_verifier path \"$dep_path\" resolves to $dep_physical, not this checkout ($root_physical)" + fi +} + +preflight_ivc_trace() { + [[ "$RUN_TRACE" -eq 1 ]] || return 0 + [[ "$PRECHECK_BUILDS" -eq 1 ]] || return 0 + + local args=(--check-only --trace --no-gas-checkpoints --srs-dir "$SRS_DIR") + if [[ "$SKIP_SRS_DOWNLOAD" -eq 1 ]]; then + args+=(--skip-srs-download) + fi + + run_logged \ + "Preflight IVC trace equivalence and SRS assets" \ + env SOLC="$SOLC" "$ROOT_DIR/scripts/run_ivc_bench.sh" "${args[@]}" +} + +preflight_moonlight() { + [[ "$RUN_MOONLIGHT" -eq 1 ]] || return 0 + [[ "$PRECHECK_BUILDS" -eq 1 ]] || return 0 + + run_logged \ + "Preflight Moonlight wrap bench compile" \ + env SOLC="$SOLC" cargo test \ + --manifest-path "$(moonlight_manifest_path)" \ + "$MOONLIGHT_WRAP_TEST" \ + --release \ + --lib \ + --no-run +} + +run_trace_equivalence() { + [[ "$RUN_TRACE" -eq 1 ]] || return 0 + + local args=(--trace --no-gas-checkpoints --srs-dir "$SRS_DIR") + if [[ "$SKIP_SRS_DOWNLOAD" -eq 1 ]]; then + args+=(--skip-srs-download) + fi + + run_logged \ + "IVC Rust/Solidity trace equivalence" \ + env SOLC="$SOLC" "$ROOT_DIR/scripts/run_ivc_bench.sh" "${args[@]}" +} + +run_moonlight_bench() { + [[ "$RUN_MOONLIGHT" -eq 1 ]] || return 0 + + run_logged \ + "Moonlight wrap decider Solidity bench and trace equivalence" \ + env \ + SOLC="$SOLC" \ + MOONLIGHT_RUN_WRAP_SOLIDITY_BENCH=1 \ + MOONLIGHT_RUN_WRAP_SOLIDITY_TRACE=1 \ + MOONLIGHT_WRAP_SOLIDITY_DUMP_DIR="$MOONLIGHT_DUMP_DIR" \ + cargo test \ + --manifest-path "$(moonlight_manifest_path)" \ + "$MOONLIGHT_WRAP_TEST" \ + --release \ + --lib \ + -- \ + --ignored \ + --nocapture +} + +print_intro() { + printf '%s\n' "${BOLD}${BLUE}Halo2 Solidity team demo runner${RESET}" + printf '%s\n' "Repo: $ROOT_DIR" + printf '%s\n' "SRS_DIR: $SRS_DIR" + printf '%s\n' "Logs: $LOG_DIR" + printf '%s\n' "Moonlight: $MOONLIGHT_DIR" + printf '%s\n' "Branch: $MOONLIGHT_BRANCH" + if env_flag_enabled "$ALLOW_UNPINNED_SOLC"; then + printf '%s\n' "Solc pin: disabled" + fi + if [[ -n "${RUSTUP_TOOLCHAIN:-}" ]]; then + printf '%s\n' "Rust: RUSTUP_TOOLCHAIN=$RUSTUP_TOOLCHAIN" + fi + printf '\n' + printf '%s\n' "${YELLOW}This can take a while.${RESET} The trace run and Moonlight wrap bench both generate real proofs." + printf '%s\n' "Use --check-only for setup and compile checks without the slow proof runs." + printf '\n' + print_moonlight_gas_command +} + +print_done() { + section "Done" + ok "Logs are under $LOG_DIR" + if [[ "$RUN_TRACE" -eq 1 && "$CHECK_ONLY" -eq 0 ]]; then + ok "Trace equivalence artifacts: $ROOT_DIR/target/ivc-keccak-solidity-dump" + fi + if [[ "$RUN_MOONLIGHT" -eq 1 && "$CHECK_ONLY" -eq 0 ]]; then + ok "Moonlight Solidity artifacts: $MOONLIGHT_DUMP_DIR" + fi +} + +while (($#)); do + case "$1" in + --check-only) + CHECK_ONLY=1 + ;; + --skip-srs-download) + SKIP_SRS_DOWNLOAD=1 + ;; + --srs-dir) + [[ $# -ge 2 ]] || die "--srs-dir requires DIR" + SRS_DIR="$2" + shift + ;; + --log-dir) + [[ $# -ge 2 ]] || die "--log-dir requires DIR" + LOG_DIR="$2" + shift + ;; + --moonlight-dir) + [[ $# -ge 2 ]] || die "--moonlight-dir requires DIR" + MOONLIGHT_DIR="$2" + shift + ;; + --moonlight-branch) + [[ $# -ge 2 ]] || die "--moonlight-branch requires NAME" + MOONLIGHT_BRANCH="$2" + shift + ;; + --moonlight-url) + [[ $# -ge 2 ]] || die "--moonlight-url requires URL" + MOONLIGHT_URL="$2" + shift + ;; + --https) + USE_HTTPS=1 + ;; + --update-moonlight) + UPDATE_MOONLIGHT=1 + ;; + --no-clone-moonlight) + CLONE_MOONLIGHT=0 + ;; + --skip-trace) + RUN_TRACE=0 + ;; + --skip-moonlight) + RUN_MOONLIGHT=0 + ;; + --no-solc-install) + INSTALL_SOLC=0 + ;; + --allow-unpinned-solc) + ALLOW_UNPINNED_SOLC=1 + ;; + --rust-toolchain) + [[ $# -ge 2 ]] || die "--rust-toolchain requires NAME" + RUST_TOOLCHAIN="$2" + shift + ;; + --no-precheck-builds) + PRECHECK_BUILDS=0 + ;; + --no-fix-moonlight-dep) + FIX_MOONLIGHT_DEP=0 + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "unknown option: $1" + ;; + esac + shift +done + +SRS_DIR="$(abs_path "$SRS_DIR")" +LOG_DIR="$(abs_path "$LOG_DIR")" +MOONLIGHT_DUMP_DIR="$(abs_path "$MOONLIGHT_DUMP_DIR")" +MOONLIGHT_DIR="$(abs_path "$MOONLIGHT_DIR")" + +if env_flag_enabled "$ALLOW_UNPINNED_SOLC"; then + export HALO2_SOLIDITY_ALLOW_UNPINNED_SOLC=1 +fi +if [[ -n "$RUST_TOOLCHAIN" ]]; then + export RUSTUP_TOOLCHAIN="$RUST_TOOLCHAIN" +fi + +print_intro +check_tools +ensure_solc +if [[ "$RUN_MOONLIGHT" -eq 1 ]]; then + setup_moonlight +fi +preflight_ivc_trace +preflight_moonlight + +if [[ "$CHECK_ONLY" -eq 1 ]]; then + section "Check-only complete" + ok "Setup and compile preflights completed. Re-run without --check-only for the slow proof benches." + exit 0 +fi + +run_trace_equivalence +run_moonlight_bench +print_done diff --git a/proofs/solidity-verifier/src/api.rs b/proofs/solidity-verifier/src/api.rs new file mode 100644 index 000000000..bbe20325a --- /dev/null +++ b/proofs/solidity-verifier/src/api.rs @@ -0,0 +1,588 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Public API surface for the Solidity verifier crate. +//! +//! This module contains caller-facing configuration, diagnostics, and error +//! types. Keeping them outside the private codegen tree preserves stable +//! crate-root exports while letting the implementation modules move around. + +use std::fmt; + +use ruint::aliases::U256; + +use crate::lowering::layout; + +/// KZG accumulator encoding information. +/// +/// Accumulator verification is opt-in. A generated verifier only batches a +/// public accumulator pairing equation into the final PLONK/KZG pairing when +/// [`GeneratorConfig::with_accumulator`] is used before constructing the +/// generator. +/// +/// The accumulator is encoded as a tail of the non-committed public-input +/// vector, starting at [`Self::offset`]. The default Solidity decoder supports +/// Midnight's fully-collapsed BLS12-381 accumulator layout: +/// +/// ```text +/// lhs point coordinates, lhs scalar, rhs point coordinates, rhs scalar +/// ``` +/// +/// with each BLS12-381 base-field coordinate represented as seven radix-2^56 +/// limbs, packed four limbs per public-input field element. Any public-input +/// words after the fixed accumulator payload are interpreted as the optional +/// RHS fixed-base scalar tail for partially collapsed accumulators. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AccumulatorEncoding { + /// Offset of accumulator limbs in instances. + pub offset: usize, + /// Number of limbs per base field element. + pub num_limbs: usize, + /// Number of bits per limb. + pub num_limb_bits: usize, + /// Public-input accumulator layout. + pub kind: AccumulatorEncodingKind, +} + +/// Public-input layouts supported by the generated accumulator checker. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AccumulatorEncodingKind { + /// `AssignedAccumulator::as_public_input`: lhs point, lhs scalar, rhs + /// point, rhs scalar, followed by an optional fixed-base scalar tail. + PointAndScalar, + /// Already collapsed point-pair layout: lhs point, rhs point. The generated + /// verifier treats both carried scalars as one and does not accept a + /// fixed-base scalar tail. + PointPair, +} + +/// Committed-instance commitment policy supported by this generator. +/// +/// The current Solidity verifier shape mirrors the Midfall/zk-stdlib path +/// where the committed-instance commitment absorbed into the transcript is the +/// G1 identity. It is not a generic committed-instance-column verifier for +/// caller-supplied commitments. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CommittedInstanceCommitmentKind { + /// Absorb and use `G1::identity()` for every committed instance column. + Identity, +} + +impl AccumulatorEncoding { + /// Supported limb count for one BLS12-381 base-field coordinate. + pub const SUPPORTED_NUM_LIMBS: usize = layout::accumulator::LIMBS; + /// Supported bits per accumulator limb. + pub const SUPPORTED_NUM_LIMB_BITS: usize = layout::accumulator::LIMB_BITS; + /// Public-input words required by the fully collapsed accumulator form. + pub const FULLY_COLLAPSED_PUBLIC_INPUT_WORDS: usize = 10; + /// Public-input words required by an already collapsed `(lhs, rhs)` point + /// pair. + pub const POINT_PAIR_PUBLIC_INPUT_WORDS: usize = 8; + + /// Return a new `AccumulatorEncoding`. + pub fn new(offset: usize, num_limbs: usize, num_limb_bits: usize) -> Self { + Self { + offset, + num_limbs, + num_limb_bits, + kind: AccumulatorEncodingKind::PointAndScalar, + } + } + + /// Return a point-pair accumulator encoding with implicit unit scalars. + pub fn point_pair(offset: usize, num_limbs: usize, num_limb_bits: usize) -> Self { + Self { + offset, + num_limbs, + num_limb_bits, + kind: AccumulatorEncodingKind::PointPair, + } + } + + /// Number of public-input words needed for one base-field coordinate. + fn coordinate_words(self) -> usize { + let limbs_per_instance = (254 / self.num_limb_bits).max(1); + self.num_limbs.div_ceil(limbs_per_instance) + } + + /// Number of public-input words for two G1 coordinates. + fn point_pair_words(self) -> usize { + 4 * self.coordinate_words() + } + + /// Whether this public-input layout carries explicit lhs/rhs scalars. + pub(crate) fn has_carried_scalars(self) -> bool { + matches!(self.kind, AccumulatorEncodingKind::PointAndScalar) + } + + /// Number of public-input words occupied by the fixed accumulator payload. + fn fixed_payload_words(self) -> usize { + self.point_pair_words() + + if self.has_carried_scalars() { + layout::accumulator::CARRIED_SCALARS + } else { + 0 + } + } + + /// Minimum number of public-input words occupied by the accumulator tail, + /// excluding any optional fixed-base scalar tail. + pub fn fully_collapsed_public_input_words(self) -> Result { + self.validate_for_num_instances(usize::MAX)?; + Ok(self.fixed_payload_words()) + } + + /// Validate this encoding against the generated verifier's supported + /// schema. + pub(crate) fn validate_for_num_instances( + self, + num_instances: usize, + ) -> Result<(), GeneratorError> { + if self.num_limbs != Self::SUPPORTED_NUM_LIMBS + || self.num_limb_bits != Self::SUPPORTED_NUM_LIMB_BITS + { + return Err(GeneratorError::UnsupportedAccumulatorEncoding { + offset: self.offset, + num_limbs: self.num_limbs, + num_limb_bits: self.num_limb_bits, + num_instances, + reason: "expected 7 radix-2^56 limbs per BLS12-381 base-field coordinate", + }); + } + + let required_words = self.fixed_payload_words(); + if self.offset.saturating_add(required_words) > num_instances { + return Err(GeneratorError::UnsupportedAccumulatorEncoding { + offset: self.offset, + num_limbs: self.num_limbs, + num_limb_bits: self.num_limb_bits, + num_instances, + reason: "accumulator public-input tail exceeds num_instances", + }); + } + + Ok(()) + } + + /// Number of optional fixed-base accumulator scalars after the fixed + /// payload. + pub(crate) fn fixed_scalar_count(self, num_instances: usize) -> Result { + self.validate_for_num_instances(num_instances)?; + let tail = num_instances - (self.offset + self.fixed_payload_words()); + if self.kind == AccumulatorEncodingKind::PointPair && tail != 0 { + return Err(GeneratorError::UnsupportedAccumulatorEncoding { + offset: self.offset, + num_limbs: self.num_limbs, + num_limb_bits: self.num_limb_bits, + num_instances, + reason: "point-pair accumulator encoding does not support a fixed-base scalar tail", + }); + } + Ok(tail) + } +} + +/// Stable diagnostic view of the identities folded into the quotient numerator. +/// +/// This is not part of the Solidity verifier ABI. It is a host-side inspection +/// API for confirming which custom gates and argument identities a generated +/// verifier will evaluate. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct QuotientIdentityManifest { + /// Identities in the exact global `y`-batch order used by the verifier. + pub entries: Vec, + /// Number of normal custom-gate polynomial identities. + pub gate_identities: usize, + /// Number of permutation identities. + pub permutation_identities: usize, + /// Number of lookup identities. + pub lookup_identities: usize, + /// Number of trash argument identities. + pub trash_identities: usize, + /// Fixed-column indices for simple selector buckets, sorted by column. + pub simple_selector_cols: Vec, +} + +/// One identity in the quotient numerator manifest. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct QuotientIdentityManifestEntry { + /// Position in the global `y`-batch. + pub global_index: usize, + /// Source family and source-local metadata. + pub source: QuotientIdentitySource, + /// Accumulation target for this identity. + pub target: QuotientIdentityManifestTarget, +} + +/// Source family for a quotient numerator identity. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum QuotientIdentitySource { + /// A normal custom-gate polynomial from `vk.cs().gates()`. + Gate { + /// Index in `ConstraintSystem::gates()`. + gate_index: usize, + /// Gate name recorded by `create_gate`. + gate_name: String, + /// Constraint/polynomial index inside the gate. + constraint_index: usize, + /// Constraint name recorded by the gate builder. + constraint_name: String, + /// Polynomial index inside the gate. + polynomial_index: usize, + }, + /// A permutation argument identity. + Permutation { + /// Identity index inside the permutation family. + identity_index: usize, + }, + /// A LogUp lookup argument identity. + Lookup { + /// Identity index inside the lookup family. + identity_index: usize, + /// Lookup argument index. + lookup_index: usize, + /// Lookup name recorded by the constraint system. + lookup_name: String, + }, + /// A trash argument identity. + Trash { + /// Trash argument index. + trash_index: usize, + /// Trash argument name, usually the source additive-selector gate name. + trash_name: String, + }, +} + +/// Destination of one manifest identity after its `y` position is consumed. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum QuotientIdentityManifestTarget { + /// Fully evaluated identity accumulated into the quotient numerator scalar. + Main, + /// Simple-selector identity accumulated into a selector commitment bucket. + Selector { + /// Bucket index in the sorted simple-selector list. + selector_index: usize, + /// Fixed column backing that simple selector. + fixed_column: usize, + }, +} + +/// Immutable generator configuration. +/// +/// The constructor validates this entire shape up front. Post-construction +/// mutation is intentionally not supported, so render/repack paths always use +/// the same public-instance and accumulator schema. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct GeneratorConfig { + /// Number of non-committed public-input values expected by `verifyProof`. + pub num_instances: usize, + /// Number of instance columns committed to in the proof transcript. + pub num_committed_instances: usize, + /// Optional public accumulator pairing-batch encoding. + pub accumulator: Option, +} + +impl GeneratorConfig { + /// Construct a generator config without accumulator verification. + pub fn new(num_instances: usize, num_committed_instances: usize) -> Self { + Self { + num_instances, + num_committed_instances, + accumulator: None, + } + } + + /// Enable accumulator verification for this config. + pub fn with_accumulator(mut self, accumulator: AccumulatorEncoding) -> Self { + self.accumulator = Some(accumulator); + self + } +} + +/// Whether the generated verifier embeds the VK or links to a separate VK +/// contract. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum RenderVk { + /// Embed the generated VK payload directly in `Halo2Verifier.sol`. + #[default] + Embedded, + /// Render `Halo2Verifier.sol` plus `Halo2VerifyingKey.sol`. + Separate, +} + +/// Quotient numerator rendering strategy. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum RenderQuotient { + /// Render quotient reconstruction inside the verifier. + #[default] + Inline, + /// Link to a separately deployed, hard-pinned quotient evaluator. + ExternalPinned { + /// Expected deployed runtime byte length. + runtime_len: usize, + /// Expected deployed runtime codehash. + codehash: U256, + }, +} + +/// Diagnostic render knobs. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct RenderDiagnostics { + /// Emit trace logs. + pub trace: bool, + /// Emit section gas checkpoints. + pub gas_checkpoints: bool, +} + +impl Default for RenderDiagnostics { + /// Default diagnostics are feature-gated so normal library callers get the + /// same render shape as the crate was compiled for. + fn default() -> Self { + Self { + trace: crate::SOLIDITY_TRACE_ENABLED, + gas_checkpoints: crate::SOLIDITY_GAS_CHECKPOINTS_ENABLED, + } + } +} + +/// Main render options for generated Solidity artifacts. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct RenderOptions { + /// VK artifact mode. + pub vk: RenderVk, + /// Quotient numerator artifact mode. + pub quotient: RenderQuotient, + /// Trace/gas diagnostic knobs. + pub diagnostics: RenderDiagnostics, +} + +/// Rendered Solidity artifacts. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct RenderedArtifacts { + /// Always-rendered verifier source. + pub verifier: String, + /// Separate VK source when [`RenderVk::Separate`] is selected. + pub verifying_key: Option, + /// Quotient evaluator source when [`RenderQuotient::ExternalPinned`] is + /// selected. + pub quotient_evaluator: Option, +} + +/// Errors from native-proof to Solidity-proof repacking. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum RepackError { + /// The native compressed proof does not match the generated proof schema. + LengthMismatch { + /// Expected native compressed proof byte length. + expected: usize, + /// Actual input byte length. + actual: usize, + /// Number of prefix compressed G1 commitments before scalar evals. + prefix_g1: usize, + /// Number of main evaluation scalars. + num_evals: usize, + /// Number of PCS point-set evaluation scalars. + num_point_sets: usize, + }, + /// One compressed G1 commitment failed host-side decompression. + InvalidCompressedG1 { + /// Byte offset of the failing compressed G1 in the native proof. + offset: usize, + /// Hex encoding of the failing 48-byte compressed G1. + bytes_hex: String, + }, +} + +impl fmt::Display for RepackError { + /// Format repack failures with byte-level context useful to callers. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::LengthMismatch { + expected, + actual, + prefix_g1, + num_evals, + num_point_sets, + } => write!( + f, + "compressed proof length mismatch: expected {expected} bytes (prefix_g1={prefix_g1}, num_evals={num_evals}, num_point_sets={num_point_sets}, +f_com+pi), got {actual}" + ), + Self::InvalidCompressedG1 { offset, bytes_hex } => write!( + f, + "invalid compressed G1 at compressed[{offset}..{}]: bytes = 0x{bytes_hex}", + offset + layout::G1_COMPRESSED_BYTES + ), + } + } +} + +impl std::error::Error for RepackError {} + +/// Errors returned when a constraint system is outside the currently +/// supported Midfall Solidity verifier shape, or when generation/repacking +/// fails for the configured shape. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum GeneratorError { + /// A verifier with no advice commitments has no proof commitment phase to + /// bind into the Fiat-Shamir transcript. + NoAdviceColumns, + /// The generated transcript and instance-evaluation path currently + /// supports exactly one committed identity column and one non-committed + /// public-input column. + UnsupportedInstanceColumnShape { + total: usize, + committed: usize, + expected_committed: usize, + expected_non_committed: usize, + }, + /// Instance columns are read as direct public inputs and locally + /// Lagrange-interpolated only at the current row. + RotatedInstanceQuery { column: usize, rotation: i32 }, + /// The optional public accumulator pairing batch currently supports only + /// the Midnight BLS12-381 public-input encoding used by the IVC decider + /// fixtures. + UnsupportedAccumulatorEncoding { + offset: usize, + num_limbs: usize, + num_limb_bits: usize, + num_instances: usize, + reason: &'static str, + }, + /// Internal render/layout planning failed before Solidity was emitted. + Planning { + /// Planning stage. + stage: &'static str, + /// Human-readable error. + message: String, + }, + /// Formatting the Askama model into a Solidity string failed. + Render { + /// Artifact being rendered. + artifact: &'static str, + }, + /// Repacking a native proof into the Solidity ABI failed. + Repack(RepackError), +} + +impl fmt::Display for GeneratorError { + /// Format typed generator errors as caller-facing diagnostics. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NoAdviceColumns => { + write!(f, "at least one advice column is required") + } + Self::UnsupportedInstanceColumnShape { + total, + committed, + expected_committed, + expected_non_committed, + } => { + let non_committed = total.checked_sub(*committed).map_or_else( + || format!("invalid: committed {committed} exceeds total {total}"), + |n| n.to_string(), + ); + write!( + f, + "unsupported instance column shape: got total={total}, committed={committed}, non_committed={non_committed}; expected exactly {expected_committed} identity-committed and {expected_non_committed} non-committed" + ) + } + Self::RotatedInstanceQuery { column, rotation } => write!( + f, + "rotated instance query is not supported: column {column}, rotation {rotation}" + ), + Self::UnsupportedAccumulatorEncoding { + offset, + num_limbs, + num_limb_bits, + num_instances, + reason, + } => write!( + f, + "unsupported accumulator encoding: offset={offset}, num_limbs={num_limbs}, num_limb_bits={num_limb_bits}, num_instances={num_instances}; {reason}" + ), + Self::Planning { stage, message } => { + write!(f, "generator planning failed during {stage}: {message}") + } + Self::Render { artifact } => write!(f, "failed to render {artifact}"), + Self::Repack(err) => write!(f, "{err}"), + } + } +} + +impl std::error::Error for GeneratorError {} + +impl From for GeneratorError { + /// Promote repacking failures into the generator's public error type. + fn from(err: RepackError) -> Self { + Self::Repack(err) + } +} + +/// Field-evaluation counts for the proof layout consumed by the generated +/// Solidity verifier. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct ProofEvaluationCounts { + /// Committed instance-query evaluations read from the proof. + pub committed_instance: usize, + /// Non-committed instance-query evaluations reconstructed from calldata + /// public inputs, not read from the proof. + pub computed_instance: usize, + /// Advice-query evaluations read from the proof. + pub advice: usize, + /// Non-simple fixed-column evaluations read from the proof. + pub fixed: usize, + /// Simple selector fixed columns synthesized/handled via selector + /// commitments instead of proof eval scalars. + pub simple_selector_fixed: usize, + /// Permutation common/sigma evaluations read from the proof. + pub permutation_common: usize, + /// Permutation product evaluations (`z_cur`, `z_next`, and non-final + /// `z_last`) read from the proof. + pub permutation_product: usize, + /// Number of permutation product sets. + pub permutation_sets: usize, + /// Lookup multiplicity evaluations read from the proof. + pub lookup_multiplicity: usize, + /// Lookup helper evaluations read from the proof. + pub lookup_helper: usize, + /// Lookup accumulator evaluations (`z`, `z_next`) read from the proof. + pub lookup_accumulator: usize, + /// Trash argument evaluations read from the proof. + pub trash: usize, + /// Dummy eval scalars appended for the `fewer-point-sets` PCS layout. + pub dummy: usize, +} + +impl ProofEvaluationCounts { + /// Main proof eval scalars, excluding dummy PCS evals. + pub fn proof_main_total(&self) -> usize { + self.committed_instance + + self.advice + + self.fixed + + self.permutation_common + + self.permutation_product + + self.lookup_multiplicity + + self.lookup_helper + + self.lookup_accumulator + + self.trash + } + + /// All proof eval scalars consumed by the generated Solidity verifier. + pub fn proof_total(&self) -> usize { + self.proof_main_total() + self.dummy + } + + /// Instance evals available to identity reconstruction, including those + /// computed locally from public inputs. + pub fn instance_total_for_identities(&self) -> usize { + self.committed_instance + self.computed_instance + } + + /// Total permutation eval scalars read from the proof. + pub fn permutation_total(&self) -> usize { + self.permutation_common + self.permutation_product + } + + /// Total lookup eval scalars read from the proof. + pub fn lookup_total(&self) -> usize { + self.lookup_multiplicity + self.lookup_helper + self.lookup_accumulator + } +} diff --git a/proofs/solidity-verifier/src/builder/api.rs b/proofs/solidity-verifier/src/builder/api.rs new file mode 100644 index 000000000..497a59f81 --- /dev/null +++ b/proofs/solidity-verifier/src/builder/api.rs @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Public and crate-local constructor/configuration methods for +//! `SolidityGenerator`. +//! +//! This slice owns validation of the supported proof shape and exposes the +//! stable caller-facing knobs before lower-level generation starts. + +use super::*; + +impl<'a> SolidityGenerator<'a> { + /// Number of committed instance columns supported by the generated ABI. + pub(super) const SUPPORTED_COMMITTED_INSTANCE_COLUMNS: usize = 1; + /// Number of non-committed instance columns supported by the generated ABI. + pub(super) const SUPPORTED_NON_COMMITTED_INSTANCE_COLUMNS: usize = 1; + /// Committed-instance commitment policy hard-coded by the generated + /// verifier ABI. + pub const SUPPORTED_COMMITTED_INSTANCE_COMMITMENT: CommittedInstanceCommitmentKind = + CommittedInstanceCommitmentKind::Identity; + + /// Return a new `SolidityGenerator`. + pub fn new( + params: &'a ParamsKZG, + vk: &'a VerifyingKey>, + config: GeneratorConfig, + ) -> Self { + Self::try_new(params, vk, config) + .unwrap_or_else(|err| panic!("unsupported Solidity verifier shape: {err}")) + } + + /// Try to construct a new `SolidityGenerator`, returning a typed error + /// when the supplied constraint system is outside the currently supported + /// Midfall verifier shape. + pub fn try_new( + params: &'a ParamsKZG, + vk: &'a VerifyingKey>, + config: GeneratorConfig, + ) -> Result { + if vk.cs().num_advice_columns() == 0 { + return Err(GeneratorError::NoAdviceColumns); + } + // Midfall's Rust verifier receives instances in two arguments: + // committed instances and normal (non-committed) instances. The + // total number of instance columns is their sum, with committed + // columns first in verifier order (`plonk/verifier.rs::verify_proof`). + Self::validate_instance_column_shape( + vk.cs().num_instance_columns(), + config.num_committed_instances, + )?; + if let Some(acc_encoding) = config.accumulator { + acc_encoding.validate_for_num_instances(config.num_instances)?; + } + if let Some((column, rotation)) = vk + .cs() + .instance_queries() + .iter() + .find(|(_, rotation)| *rotation != Rotation::cur()) + { + return Err(GeneratorError::RotatedInstanceQuery { + column: column.index(), + rotation: rotation.0, + }); + } + + let meta = ConstraintSystemMeta::new(vk.cs(), config.num_committed_instances); + + Ok(Self { + params, + vk, + num_instances: config.num_instances, + acc_encoding: config.accumulator, + meta, + }) + } + + /// Return the committed-instance commitment policy for this generator. + pub fn committed_instance_commitment_kind(&self) -> CommittedInstanceCommitmentKind { + Self::SUPPORTED_COMMITTED_INSTANCE_COMMITMENT + } + + /// Validate the currently supported committed/non-committed instance split. + pub(super) fn validate_instance_column_shape( + total_instance_columns: usize, + num_committed_instances: usize, + ) -> Result<(), GeneratorError> { + // The Rust verifier accepts committed and normal instance arguments + // separately, with committed instance columns first. The current + // Solidity calldata ABI supports exactly one identity-committed column + // and one direct public-input column, so every instance query can be + // classified without an extra column-routing table or supplied + // committed-instance commitment. + let supported_committed = Self::SUPPORTED_COMMITTED_INSTANCE_COLUMNS; + let supported_non_committed = Self::SUPPORTED_NON_COMMITTED_INSTANCE_COLUMNS; + let non_committed = total_instance_columns + .checked_sub(num_committed_instances) + .unwrap_or(usize::MAX); + + if num_committed_instances != supported_committed + || non_committed != supported_non_committed + { + return Err(GeneratorError::UnsupportedInstanceColumnShape { + total: total_instance_columns, + committed: num_committed_instances, + expected_committed: supported_committed, + expected_non_committed: supported_non_committed, + }); + } + + Ok(()) + } + + /// Return the exact field-evaluation counts for the proof layout consumed + /// by the generated Solidity verifier. + pub fn proof_evaluation_counts(&self) -> ProofEvaluationCounts { + crate::lowering::diagnostics::proof_evaluation_counts(self.inputs()) + } + + /// Return a stable host-side manifest of quotient numerator identities. + /// + /// This diagnostic API follows the same source ordering as the generated + /// quotient evaluator: normal gates, permutation, lookup, then trash. + pub fn quotient_identity_manifest(&self) -> QuotientIdentityManifest { + crate::lowering::diagnostics::quotient_identity_manifest(self.inputs()) + } +} diff --git a/proofs/solidity-verifier/src/builder/mod.rs b/proofs/solidity-verifier/src/builder/mod.rs new file mode 100644 index 000000000..640880c3a --- /dev/null +++ b/proofs/solidity-verifier/src/builder/mod.rs @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Thin public facade for rendering a concrete Solidity verifier. +//! +//! `builder` owns the public `SolidityGenerator` type, supported-shape +//! validation, and caller-facing wrappers. The actual lowering pipeline lives +//! in `lowering`, which receives read-only verifier build inputs. + +use std::fmt::Debug; + +use midnight_curves::{Bls12, Fq}; +use midnight_proofs::{ + plonk::VerifyingKey, + poly::{ + kzg::{params::ParamsKZG, KZGCommitmentScheme}, + Rotation, + }, +}; + +use crate::{ + api::{ + AccumulatorEncoding, CommittedInstanceCommitmentKind, GeneratorConfig, GeneratorError, + ProofEvaluationCounts, QuotientIdentityManifest, RenderDiagnostics, RenderOptions, + RenderedArtifacts, RepackError, + }, + lowering::{encoding::ConstraintSystemMeta, VerifierBuildInputs}, +}; + +mod api; +mod render; +mod repack; + +#[cfg(test)] +#[allow(clippy::items_after_test_module)] +mod tests; + +/// Solidity verifier generator for midnight-proofs (logup + trash + KZG +/// multi-prepare PCS) on BLS12-381 EIP-2537. +/// +/// The supported protocol shape is intentionally narrow: Midfall/Midnight +/// KZG proofs with one committed identity instance column and one +/// non-committed public-input column. +#[derive(Debug)] +pub struct SolidityGenerator<'a> { + params: &'a ParamsKZG, + vk: &'a VerifyingKey>, + num_instances: usize, + /// Optional accumulator layout encoded at the tail of the public instances. + acc_encoding: Option, + meta: ConstraintSystemMeta, +} + +impl<'a> SolidityGenerator<'a> { + /// Package immutable generator state for the private lowering pipeline. + /// + /// The returned value borrows the constructor inputs and the cached + /// constraint-system metadata; each render/repack path then builds its own + /// converged lowering plan from the same facts. + pub(crate) fn inputs(&self) -> VerifierBuildInputs<'a, '_> { + VerifierBuildInputs { + params: self.params, + vk: self.vk, + num_instances: self.num_instances, + num_committed_instances: self.meta.num_committed_instances, + acc_encoding: self.acc_encoding, + meta: &self.meta, + } + } +} diff --git a/proofs/solidity-verifier/src/builder/render.rs b/proofs/solidity-verifier/src/builder/render.rs new file mode 100644 index 000000000..7d3eee235 --- /dev/null +++ b/proofs/solidity-verifier/src/builder/render.rs @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Options-driven rendering entry points for `SolidityGenerator`. + +use ruint::aliases::U256; + +use super::*; +use crate::{ + api::{RenderQuotient, RenderVk}, + lowering::{plan::LoweringPlan, render::Halo2VerifyingKey}, +}; + +struct VerifierRenderPlan { + separate: bool, + trace: bool, + gas_checkpoints: bool, + external_quotient: bool, + expected_quotient: Option<(usize, U256)>, +} + +impl<'a> SolidityGenerator<'a> { + /// Render generated Solidity artifacts according to one immutable options + /// value. + pub fn render(&self, options: RenderOptions) -> Result { + let separate = matches!(options.vk, RenderVk::Separate); + let (external_quotient, expected_quotient) = match options.quotient { + RenderQuotient::Inline => (false, None), + RenderQuotient::ExternalPinned { + runtime_len, + codehash, + } => (true, Some((runtime_len, codehash))), + }; + + let inputs = self.inputs(); + let plan = inputs.lowering_plan(); + + let render_plan = VerifierRenderPlan { + separate, + trace: options.diagnostics.trace, + gas_checkpoints: options.diagnostics.gas_checkpoints, + external_quotient, + expected_quotient, + }; + let verifier = self.render_verifier_source_with_plan(&inputs, &plan, render_plan)?; + + let verifying_key = separate.then(|| Self::render_vk_model(&plan.vk)).transpose()?; + let quotient_evaluator = external_quotient + .then(|| self.render_quotient_evaluator_with_plan(&inputs, &plan, options.diagnostics)) + .transpose()?; + + Ok(RenderedArtifacts { + verifier, + verifying_key, + quotient_evaluator, + }) + } + + /// Render only `Halo2QuotientEvaluator.sol`. + /// + /// Pinned-quotient deployment flows compile/deploy this source first, + /// compute its runtime length and codehash, then render the verifier with + /// [`RenderQuotient::ExternalPinned`]. + pub fn render_quotient_evaluator( + &self, + diagnostics: RenderDiagnostics, + ) -> Result { + let inputs = self.inputs(); + let plan = inputs.lowering_plan(); + self.render_quotient_evaluator_with_plan(&inputs, &plan, diagnostics) + } + + /// Render the split quotient evaluator from a caller-provided converged + /// plan. + /// + /// Keeping this helper plan-parameterized prevents the verifier render and + /// evaluator render from silently planning different memory/quotient facts + /// inside one `render` call. + fn render_quotient_evaluator_with_plan( + &self, + inputs: &VerifierBuildInputs<'_, '_>, + plan: &LoweringPlan, + diagnostics: RenderDiagnostics, + ) -> Result { + let mut quotient_output = String::new(); + inputs + .generate_quotient_evaluator_from_plan(plan, diagnostics.trace) + .render(&mut quotient_output) + .map_err(|_| GeneratorError::Render { + artifact: "Halo2QuotientEvaluator.sol", + })?; + Ok(quotient_output) + } + + /// Render the main verifier source from the same converged plan. + /// + /// `expected_quotient` is present only after the external evaluator has + /// been compiled/deployed by the caller and its runtime hash is known. + fn render_verifier_source_with_plan( + &self, + inputs: &VerifierBuildInputs<'_, '_>, + plan: &LoweringPlan, + render_plan: VerifierRenderPlan, + ) -> Result { + let mut verifier_output = String::new(); + inputs + .generate_verifier_from_plan( + plan, + render_plan.separate, + render_plan.trace, + render_plan.gas_checkpoints, + render_plan.external_quotient, + render_plan.expected_quotient, + ) + .render(&mut verifier_output) + .map_err(|_| GeneratorError::Render { + artifact: "Halo2Verifier.sol", + })?; + Ok(verifier_output) + } + + /// Render the separate verifying-key payload contract. + fn render_vk_model(vk: &Halo2VerifyingKey) -> Result { + let mut vk_output = String::new(); + vk.render(&mut vk_output).map_err(|_| GeneratorError::Render { + artifact: "Halo2VerifyingKey.sol", + })?; + Ok(vk_output) + } +} diff --git a/proofs/solidity-verifier/src/builder/repack.rs b/proofs/solidity-verifier/src/builder/repack.rs new file mode 100644 index 000000000..5333facba --- /dev/null +++ b/proofs/solidity-verifier/src/builder/repack.rs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Public calldata helpers for `SolidityGenerator`. + +use super::*; + +impl<'a> SolidityGenerator<'a> { + /// Repack a midnight-proofs proof into the generated verifier ABI. + pub fn repack_proof(&self, compressed: &[u8]) -> Result, RepackError> { + self.inputs().repack_proof(compressed) + } + + /// Repack a native proof and encode verifier calldata for `verifyProof`. + pub fn encode_calldata( + &self, + compressed_proof: &[u8], + instances: &[Fq], + ) -> Result, GeneratorError> { + self.inputs().encode_calldata(compressed_proof, instances) + } + + #[cfg(all(test, feature = "evm"))] + pub(crate) fn repacked_proof_scalar_layout_for_test( + &self, + ) -> crate::lowering::quotient_numerator::vm::RepackedProofScalarLayout { + self.inputs().repacked_proof_scalar_layout_for_test() + } +} diff --git a/proofs/solidity-verifier/src/builder/tests.rs b/proofs/solidity-verifier/src/builder/tests.rs new file mode 100644 index 000000000..90a711919 --- /dev/null +++ b/proofs/solidity-verifier/src/builder/tests.rs @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Builder facade tests. +//! +//! These tests cover public-shape validation plus a few lowering helpers that +//! used to live on `SolidityGenerator`. + +use super::SolidityGenerator; +use crate::{ + api::GeneratorError, + lowering::{ + quotient::{NativeGateCandidate, QuotientComputationBlocks}, + quotient_numerator::vm::QuotientProgramBuild, + VerifierBuildInputs, + }, +}; + +#[test] +fn instance_column_shape_validation_is_exact() { + assert!(SolidityGenerator::validate_instance_column_shape(2, 1).is_ok()); + + for (total, committed) in [(2, 0), (2, 2), (1, 1), (3, 1)] { + assert!( + matches!( + SolidityGenerator::validate_instance_column_shape(total, committed), + Err(GeneratorError::UnsupportedInstanceColumnShape { .. }) + ), + "shape total={total}, committed={committed} should be rejected" + ); + } +} + +#[test] +fn quotient_stack_words_cover_native_callback_scratch() { + let build = QuotientProgramBuild { + bytes: Vec::new(), + consts: Vec::new(), + max_stack: 3, + used_ops: Vec::new(), + used_mem_tokens: Vec::new(), + }; + + assert_eq!( + VerifierBuildInputs::quotient_stack_words_for_build(&build, 0), + 3 + ); + assert_eq!( + VerifierBuildInputs::quotient_stack_words_for_build(&build, 2), + 3 + ); + assert_eq!( + VerifierBuildInputs::quotient_stack_words_for_build(&build, 8), + 8 + ); +} + +#[test] +fn native_gate_knapsack_prefers_gas_under_byte_budget() { + let candidates = vec![ + NativeGateCandidate { + gate_idx: 0, + vm_bytes: 100, + native_bytes: 320, + gas_saved: 100, + }, + NativeGateCandidate { + gate_idx: 1, + vm_bytes: 80, + native_bytes: 160, + gas_saved: 60, + }, + NativeGateCandidate { + gate_idx: 2, + vm_bytes: 70, + native_bytes: 160, + gas_saved: 55, + }, + ]; + + let selection = VerifierBuildInputs::select_native_gate_candidates(&candidates, 2, 320); + assert_eq!(selection.gate_indices, vec![1, 2]); + assert_eq!(selection.gas_saved, 115); + assert_eq!(selection.native_bytes, 320); +} + +#[test] +fn native_gate_default_budget_preserves_old_top_n_byte_envelope() { + let candidates = vec![ + NativeGateCandidate { + gate_idx: 0, + vm_bytes: 100, + native_bytes: 300, + gas_saved: 1, + }, + NativeGateCandidate { + gate_idx: 1, + vm_bytes: 80, + native_bytes: 200, + gas_saved: 1, + }, + NativeGateCandidate { + gate_idx: 2, + vm_bytes: 70, + native_bytes: 100, + gas_saved: 1, + }, + ]; + + assert_eq!( + VerifierBuildInputs::default_native_gate_byte_budget(&candidates, 2), + 500 + ); +} + +#[test] +fn quotient_helper_flags_scan_all_computation_block_families() { + let blocks = QuotientComputationBlocks { + inline_computations: vec![vec!["let a := q_pow5(x)".to_string()]], + native_lookup_computation: vec!["let b := q_limb7(x0, x1, x2, x3, x4, x5, x6)".to_string()], + native_identity_computations: vec![vec![ + "let c := q_limb7_wide(x0, x1, x2, x3, x4, x5, x6)".to_string(), + ]], + ..Default::default() + }; + + let flags = blocks.helper_flags(); + + assert!(flags.pow5); + assert!(flags.limb7); + assert!(flags.wide_limb7); +} diff --git a/proofs/solidity-verifier/src/evm.rs b/proofs/solidity-verifier/src/evm.rs new file mode 100644 index 000000000..5a3ae7fec --- /dev/null +++ b/proofs/solidity-verifier/src/evm.rs @@ -0,0 +1,500 @@ +// SPDX-License-Identifier: CC0-1.0 +//! EVM-facing helpers for calldata encoding, Solidity compilation, and revm +//! tests. +//! +//! Production callers use the calldata encoders; the `evm` feature also +//! exposes pinned-solc compilation and a small revm harness for verifier smoke +//! tests. + +use ff::PrimeField; +use itertools::chain; + +use crate::lowering::encoding::{fe_to_u256, to_u256_be_bytes}; + +/// Function signature of `verifyProof(bytes,uint256[])`. +pub const FN_SIG_VERIFY_PROOF: [u8; 4] = [0x1e, 0x8e, 0x1e, 0x13]; + +/// Encode proof into calldata to invoke `Halo2Verifier.verifyProof`. +/// +/// Generic over the instance scalar so the same helper handles both BN254 +/// and BLS12-381 instances (both fields have a 32-byte `Repr`, which is +/// what the Solidity verifier reads from calldata). +pub fn encode_calldata(proof: &[u8], instances: &[F]) -> Vec +where + F: PrimeField, + F::Repr: AsRef<[u8]>, +{ + let offset = 0x40; + let num_instances = instances.len(); + chain![ + FN_SIG_VERIFY_PROOF, // function signature + to_u256_be_bytes(offset), // offset of proof + to_u256_be_bytes(offset + 0x20 + proof.len()), // offset of instances + to_u256_be_bytes(proof.len()), // length of proof + proof.iter().cloned(), // proof + to_u256_be_bytes(num_instances), // length of instances + instances.iter().map(fe_to_u256::).flat_map(to_u256_be_bytes), // instances + ] + .collect() +} + +#[cfg(feature = "evm")] +pub(crate) mod test { + use std::{ + env, + fmt::{self, Debug, Formatter}, + io::{self, Write}, + process::{Command, Stdio}, + str, + }; + + pub use revm; + use revm::{ + db::InMemoryDB, + primitives::{Address, ExecutionResult, Log, Output, SpecId, TxKind}, + Evm as RevmEvm, + }; + use ruint::aliases::U256; + use sha3::{Digest, Keccak256}; + + /// Default `--optimize-runs` value used by `compile_solidity`. + /// + /// Pre-Step 5 the verifier inlined `decompress_g1` at 21 sites, and + /// running the optimizer with `runs=200` blew past EIP-170's 24 kB + /// contract limit; we forced `runs=1` to keep the helpers shared. + /// After Step 5 dropped the on-chain decompression entirely, that + /// constraint went away and `runs=200` is now both safe (the + /// rendered bytecode comfortably fits under 24 kB) and significantly + /// cheaper at runtime — the optimizer can deduplicate + /// `common_uncompressed_g1`, `scalar_inv`, the `ec_*` helpers, and + /// hoist `mload` traffic in the gate evaluator. + /// Audit item #7 / OPTIMISATION.md "A". + pub const DEFAULT_OPTIMIZE_RUNS: u32 = 200; + /// Exact solc compiler identity used for reproducible generated bytecode. + /// + /// The platform suffix printed by `solc --version` can differ between + /// builds of the same compiler commit, so the gate pins the semantic + /// version and upstream commit. + pub const PINNED_SOLC_VERSION: &str = "0.8.30+commit.73712a01"; + /// Environment flag that lets ad-hoc benches use the configured `solc` + /// even when it does not match [`PINNED_SOLC_VERSION`]. + /// + /// This is intentionally opt-in because generated bytecode and gas numbers + /// are only reproducible with the pinned compiler. + pub const ALLOW_UNPINNED_SOLC_ENV: &str = "HALO2_SOLIDITY_ALLOW_UNPINNED_SOLC"; + + fn env_flag_enabled(name: &str) -> bool { + matches!( + env::var(name).ok().as_deref(), + Some("1" | "true" | "TRUE" | "yes" | "YES" | "on" | "ON") + ) + } + + fn allow_unpinned_solc() -> bool { + env_flag_enabled(ALLOW_UNPINNED_SOLC_ENV) + } + + /// Return the configured solc executable path. + pub fn solc_command() -> String { + env::var("SOLC").unwrap_or_else(|_| "solc".to_string()) + } + + /// Return the `Version:` field from `solc --version`. + pub fn solc_version() -> io::Result { + let solc = solc_command(); + let output = Command::new(&solc).arg("--version").output()?; + if !output.status.success() { + return Err(io::Error::other(format!( + "{solc} --version exited with {}", + output.status + ))); + } + let stdout = str::from_utf8(&output.stdout) + .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?; + stdout + .lines() + .find_map(|line| line.strip_prefix("Version: ")) + .map(str::to_owned) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing solc Version line")) + } + + /// True when the configured solc executable matches the pinned compiler + /// version/commit, or when [`ALLOW_UNPINNED_SOLC_ENV`] explicitly allows + /// ad-hoc non-reproducible compilation. + pub fn pinned_solc_available() -> bool { + match solc_version() { + Ok(version) if version.starts_with(PINNED_SOLC_VERSION) => true, + Ok(version) if allow_unpinned_solc() => { + eprintln!( + "configured solc version {version} does not match pinned {PINNED_SOLC_VERSION}; \ + continuing because {ALLOW_UNPINNED_SOLC_ENV}=1" + ); + true + } + Ok(version) => { + eprintln!( + "configured solc version {version} does not match pinned {PINNED_SOLC_VERSION}" + ); + false + } + Err(err) => { + eprintln!("pinned solc unavailable: {err}"); + false + } + } + } + + /// Return the configured solc path after enforcing the pinned compiler, + /// unless [`ALLOW_UNPINNED_SOLC_ENV`] explicitly allows ad-hoc compilation. + fn require_pinned_solc() -> String { + let solc = solc_command(); + let version = solc_version() + .unwrap_or_else(|err| panic!("Command '{solc}' not found or unusable: {err}")); + if allow_unpinned_solc() && !version.starts_with(PINNED_SOLC_VERSION) { + eprintln!( + "warning: compiling with unpinned solc `{solc}` version `{version}`; \ + bytecode and gas are not reproducible against pinned {PINNED_SOLC_VERSION}" + ); + return solc; + } + assert!( + version.starts_with(PINNED_SOLC_VERSION), + "configured solc `{solc}` has version `{version}`, expected `{PINNED_SOLC_VERSION}`" + ); + solc + } + + /// Compile solidity with `--via-ir`, targeting Cancun bytecode (the + /// embedded revm runner is set up for the Prague hard fork which + /// supersedes Cancun + adds EIP-2537), then return creation bytecode. + /// + /// Honors the `SOLC_OPTIMIZE_RUNS` environment variable for ad-hoc + /// A/B measurement; if unset, uses `DEFAULT_OPTIMIZE_RUNS`. + /// + /// # Panics + /// Panics if executable `solc` can not be found, does not match + /// [`PINNED_SOLC_VERSION`] without [`ALLOW_UNPINNED_SOLC_ENV`], or + /// compilation fails. + pub fn compile_solidity(solidity: impl AsRef<[u8]>) -> Vec { + let runs: u32 = std::env::var("SOLC_OPTIMIZE_RUNS") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(DEFAULT_OPTIMIZE_RUNS); + compile_solidity_with_runs(solidity, runs) + } + + /// Like `compile_solidity` but with an explicit `--optimize-runs` + /// value. Useful for benchmarking the optimizer's runtime/code-size + /// tradeoff. Omits CBOR metadata so reported bytecode sizes reflect + /// executable verifier code rather than compiler provenance. + /// + /// # Panics + /// Panics if executable `solc` can not be found, does not match + /// [`PINNED_SOLC_VERSION`] without [`ALLOW_UNPINNED_SOLC_ENV`], or + /// compilation fails. + pub fn compile_solidity_with_runs(solidity: impl AsRef<[u8]>, runs: u32) -> Vec { + let runs_str = runs.to_string(); + let solc = require_pinned_solc(); + let mut process = match Command::new(&solc) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .arg("--bin") + .arg("--optimize") + .arg("--optimize-runs") + .arg(&runs_str) + .arg("--via-ir") + .arg("--evm-version") + .arg("cancun") + .arg("--no-cbor-metadata") + .arg("-") + .spawn() + { + Ok(process) => process, + Err(err) if err.kind() == io::ErrorKind::NotFound => { + panic!("Command '{solc}' not found"); + } + Err(err) => { + panic!("Failed to spawn process with command '{solc}':\n{err}"); + } + }; + process.stdin.take().unwrap().write_all(solidity.as_ref()).unwrap(); + let output = process.wait_with_output().unwrap(); + let stdout = str::from_utf8(&output.stdout).unwrap(); + if let Some(binary) = find_binary(stdout) { + binary + } else { + panic!( + "Compilation fails:\n{}", + str::from_utf8(&output.stderr).unwrap() + ) + } + } + + /// Extract creation bytecode from solc's text `--bin` output. + fn find_binary(stdout: &str) -> Option> { + let start = stdout.find("Binary:")? + 8; + Some(hex::decode(&stdout[start..stdout.len() - 1]).unwrap()) + } + + /// Result of a non-panicking EVM call. Mirrors revm's + /// `ExecutionResult` shape but flattens to what test/example code + /// usually wants: gas, optional output bytes, optional revert + /// payload, and any emitted logs. + #[derive(Debug)] + pub enum CallOutcome { + /// The call returned normally. + Success { + /// Gas consumed by the call. + gas_used: u64, + /// Return-data bytes copied from the EVM. + output: Vec, + /// Logs emitted during the call. + logs: Vec, + }, + /// The call hit a `revert` opcode. + Revert { + /// Gas consumed before the revert. + gas_used: u64, + /// Revert payload bytes (often empty for solc-generated + /// `revert(0, 0)` paths). + output: Vec, + }, + /// The call halted (out-of-gas, invalid opcode, etc.). + Halt { + /// Gas consumed before the halt. + gas_used: u64, + /// Debug string identifying the halt reason. + reason: String, + }, + } + + /// In-process EVM runner pinned to `SpecId::PRAGUE` so that the + /// EIP-2537 BLS12-381 precompiles (`0x0b`/`0x0c`/`0x0d`/`0x0e`/`0x0f`) + /// are routed to revm's bundled implementations. The runner keeps an + /// `InMemoryDB` across calls so tests can deploy once and call many + /// times. + #[derive(Default)] + pub struct Evm { + db: InMemoryDB, + } + + impl Debug for Evm { + /// Hide the in-memory revm database from debug output while still + /// satisfying the crate-wide `missing_debug_implementations` lint. + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("Evm").finish_non_exhaustive() + } + } + + impl Evm { + /// Return code_size of given address. + /// + /// # Panics + /// Panics if given address doesn't have bytecode. + pub fn code_size(&mut self, address: Address) -> usize { + self.db.accounts[&address].info.code.as_ref().map(|c| c.len()).unwrap_or(0) + } + + /// Return the Keccak256 hash of deployed runtime bytecode at `address`. + /// + /// # Panics + /// Panics if given address doesn't have bytecode. + pub fn code_hash(&mut self, address: Address) -> U256 { + let code = self.db.accounts[&address] + .info + .code + .as_ref() + .expect("address must have bytecode") + .original_bytes(); + let digest: [u8; 32] = Keccak256::digest(&code).into(); + U256::from_be_bytes(digest) + } + + /// Apply create transaction with given `bytecode` as creation bytecode. + /// Return created `address`. + /// + /// # Panics + /// Panics if execution reverts or halts unexpectedly. + pub fn create(&mut self, bytecode: Vec) -> Address { + let (_, output, _) = self.run_tx(TxKind::Create, bytecode); + match output { + Output::Create(_, Some(address)) => address, + _ => unreachable!("expected create output, got {output:?}"), + } + } + + /// Apply create transaction with an address constructor argument + /// appended to creation bytecode and return created `address`. + pub fn create_with_address_arg( + &mut self, + mut bytecode: Vec, + address_arg: Address, + ) -> Address { + bytecode.extend_from_slice( + &U256::try_from_be_slice(address_arg.as_slice()).unwrap().to_be_bytes::<0x20>(), + ); + self.create(bytecode) + } + + /// Apply create transaction with two address constructor arguments + /// appended to creation bytecode and return created `address`. + pub fn create_with_two_address_args( + &mut self, + mut bytecode: Vec, + first_address_arg: Address, + second_address_arg: Address, + ) -> Address { + for address in [first_address_arg, second_address_arg] { + bytecode.extend_from_slice( + &U256::try_from_be_slice(address.as_slice()).unwrap().to_be_bytes::<0x20>(), + ); + } + self.create(bytecode) + } + + /// Apply call transaction to given `address` with `calldata`. + /// Returns `gas_used` and `return_data`. + /// + /// # Panics + /// Panics if execution reverts or halts unexpectedly. + pub fn call(&mut self, address: Address, calldata: Vec) -> (u64, Vec) { + let (gas_used, output, _) = self.run_tx(TxKind::Call(address), calldata); + match output { + Output::Call(output) => (gas_used, output.into()), + _ => unreachable!("expected call output, got {output:?}"), + } + } + + /// Apply call transaction without panicking on revert/halt. + /// Useful for fuzzing or trace-driven debugging where we want to + /// observe failures rather than abort the run. + pub fn try_call(&mut self, address: Address, calldata: Vec) -> CallOutcome { + self.try_call_with_gas(address, calldata, 50_000_000) + } + + /// Like [`Self::try_call`] but with a caller-controlled gas ceiling. + /// Necessary for very wide circuits (e.g. the IVC verifier at + /// k = 19 with ~20+ advice columns) whose verifier doesn't fit + /// within the default 50M cap during dev. + pub fn try_call_with_gas( + &mut self, + address: Address, + calldata: Vec, + gas_limit: u64, + ) -> CallOutcome { + let db = std::mem::take(&mut self.db); + let mut evm = RevmEvm::builder() + .with_db(db) + .with_spec_id(SpecId::PRAGUE) + .modify_cfg_env(|cfg| { + cfg.limit_contract_code_size = Some(usize::MAX); + }) + .modify_tx_env(|tx| { + tx.gas_limit = gas_limit; + tx.transact_to = TxKind::Call(address); + tx.data = calldata.into(); + }) + .build(); + let result = evm.transact_commit().unwrap(); + self.db = std::mem::take(&mut evm.context.evm.db); + match result { + ExecutionResult::Success { + gas_used, + output, + logs, + .. + } => CallOutcome::Success { + gas_used, + output: match output { + Output::Call(o) => o.into(), + _ => unreachable!(), + }, + logs, + }, + ExecutionResult::Revert { gas_used, output } => CallOutcome::Revert { + gas_used, + output: output.into(), + }, + ExecutionResult::Halt { reason, gas_used } => CallOutcome::Halt { + gas_used, + reason: format!("{reason:?}"), + }, + } + } + + /// Apply call transaction and return gas, return data, and emitted + /// logs. + pub fn call_with_logs( + &mut self, + address: Address, + calldata: Vec, + ) -> (u64, Vec, Vec) { + let (gas_used, output, logs) = self.run_tx(TxKind::Call(address), calldata); + match output { + Output::Call(output) => (gas_used, output.into(), logs), + _ => unreachable!("expected call output, got {output:?}"), + } + } + + /// Build a Prague-spec EVM around the in-memory db, run the + /// configured transaction, commit, then unwrap the success result. + fn run_tx(&mut self, transact_to: TxKind, data: Vec) -> (u64, Output, Vec) { + // Take the db out so we can hand it to the builder, then put it + // back after the call. + let db = std::mem::take(&mut self.db); + let mut evm = RevmEvm::builder() + .with_db(db) + .with_spec_id(SpecId::PRAGUE) + .modify_cfg_env(|cfg| { + // Lift the EIP-170 contract size cap. The full + // BLS12-381 verifier (with inline `decompress_g1` + // calls per commitment) compiles to ~25 kB of + // bytecode, which exceeds the 24 kB mainnet + // limit; for in-process tests this is fine. + cfg.limit_contract_code_size = Some(usize::MAX); + }) + .modify_tx_env(|tx| { + // Capped at 50M gas so an infinite loop in the + // emitted Yul terminates promptly during dev + // rather than spinning revm indefinitely. + tx.gas_limit = 50_000_000; + tx.transact_to = transact_to; + tx.data = data.into(); + }) + .build(); + let result = evm.transact_commit().unwrap(); + // Recover the database for the next call. revm 19 stores the + // db deep inside `evm.context.evm.db`; the simplest way to get + // it back without moving fields out of `EvmContext` (which + // holds non-Copy state) is to swap with a dummy default. + self.db = std::mem::take(&mut evm.context.evm.db); + match result { + ExecutionResult::Success { + gas_used, + output, + logs, + .. + } => { + if !logs.is_empty() { + println!("--- logs from {} ---", logs[0].address); + for (log_idx, log) in logs.iter().enumerate() { + println!("log#{log_idx}"); + for (topic_idx, topic) in log.data.topics().iter().enumerate() { + println!(" topic{topic_idx}: {topic:?}"); + } + } + println!("--- end ---"); + } + (gas_used, output, logs) + } + ExecutionResult::Revert { gas_used, output } => { + panic!("Transaction reverts with gas_used {gas_used} and output 0x{output:x}") + } + ExecutionResult::Halt { reason, gas_used } => panic!( + "Transaction halts unexpectedly with gas_used {gas_used} and reason {reason:?}" + ), + } + } + } +} diff --git a/proofs/solidity-verifier/src/lib.rs b/proofs/solidity-verifier/src/lib.rs new file mode 100644 index 000000000..011af0a58 --- /dev/null +++ b/proofs/solidity-verifier/src/lib.rs @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Solidity verifier generator for midnight-proofs with KZG polynomial +//! commitment scheme on BLS12-381 / EIP-2537. +//! +//! Historical migration notes live in `docs/architecture/MIGRATION.md`. + +#![deny(missing_debug_implementations)] +#![deny(rustdoc::broken_intra_doc_links)] + +mod api; +mod builder; +mod evm; +mod lowering; + +#[cfg(all(test, feature = "evm"))] +mod test; + +pub use api::{ + AccumulatorEncoding, AccumulatorEncodingKind, GeneratorConfig, GeneratorError, + ProofEvaluationCounts, QuotientIdentityManifest, QuotientIdentityManifestEntry, + QuotientIdentityManifestTarget, QuotientIdentitySource, RenderDiagnostics, RenderOptions, + RenderQuotient, RenderVk, RenderedArtifacts, RepackError, +}; +pub use builder::SolidityGenerator; +pub use evm::{encode_calldata, FN_SIG_VERIFY_PROOF}; + +/// Whether the default Solidity renderer emits trace logs. +/// +/// Enable with `--features solidity-trace`. `RenderDiagnostics { trace: true, +/// .. }` still forces trace output regardless of this flag. +pub const SOLIDITY_TRACE_ENABLED: bool = cfg!(feature = "solidity-trace"); + +/// Whether the default Solidity renderer emits LOG1 gas() checkpoints at +/// section boundaries. The host-side test parses these into per-section gas +/// deltas (see `dump_gas_checkpoints` in `tests/poseidon_fixture.rs`). +/// +/// Default render paths honor `solidity-gas-checkpoints` independently from +/// `solidity-trace`, so gas attribution can be measured without trace-only +/// verifier work. `RenderDiagnostics { gas_checkpoints: true, .. }` still +/// forces checkpoint emission for profiling artifacts regardless of this flag. +pub const SOLIDITY_GAS_CHECKPOINTS_ENABLED: bool = cfg!(feature = "solidity-gas-checkpoints"); + +/// Whether the generated Solidity verifier expects the outer proof to use +/// the fewer-point-sets dummy-query PCS layout. +/// +/// This is intentionally separate from recursive/in-circuit verifier proofs: +/// the IVC benchmark can keep fewer point sets for proofs checked inside the +/// decider circuit while emitting the final Solidity-facing proof without the +/// extra dummy eval scalars. +pub const OUTER_FEWER_POINT_SETS_ENABLED: bool = cfg!(feature = "outer-fewer-point-sets"); + +/// Whether the generated Solidity verifier expects the outer proof to use +/// Midnight's single-H quotient commitment layout. +/// +/// This is intentionally outer-only. Recursive proofs checked inside the IVC +/// decider circuit remain on the multi-limb layout from `midnight-circuits`. +pub const OUTER_SINGLE_H_COMMITMENT_ENABLED: bool = cfg!(feature = "outer-single-h-commitment"); + +#[cfg(feature = "evm")] +pub use evm::test::{ + compile_solidity, compile_solidity_with_runs, pinned_solc_available, revm, solc_version, + CallOutcome, Evm, ALLOW_UNPINNED_SOLC_ENV, DEFAULT_OPTIMIZE_RUNS, PINNED_SOLC_VERSION, +}; + +/// Test-only helper that exposes the internal BLS12-381 G1 to EIP-2537 +/// hi/lo encoder so debugging examples can re-encode host-computed +/// points using the exact same pipeline the Solidity verifier consumes. +#[doc(hidden)] +pub fn __test_only_g1_to_u256s(point: &midnight_curves::G1Affine) -> [ruint::aliases::U256; 4] { + crate::lowering::encoding::g1_to_u256s(point) +} + +#[doc(hidden)] +pub fn __test_only_g2_to_u256s(point: &midnight_curves::G2Affine) -> [ruint::aliases::U256; 8] { + crate::lowering::encoding::g2_to_u256s(point) +} diff --git a/proofs/solidity-verifier/src/lowering/abi/mod.rs b/proofs/solidity-verifier/src/lowering/abi/mod.rs new file mode 100644 index 000000000..7f27eeb3d --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/abi/mod.rs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: CC0-1.0 +//! ABI planning for generated verifier calldata and transcript buffers. +//! +//! The submodules here describe byte offsets and proof sections consumed by +//! both the generator and the generated Yul. + +pub(crate) mod proof; + +pub(crate) use proof::{ProofCalldataLayout, TranscriptBufferLayout}; diff --git a/proofs/solidity-verifier/src/lowering/abi/proof.rs b/proofs/solidity-verifier/src/lowering/abi/proof.rs new file mode 100644 index 000000000..3e085d1cb --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/abi/proof.rs @@ -0,0 +1,476 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Codegen-time proof calldata and transcript-buffer layout. +//! +//! The generated Solidity verifier still walks calldata with simple Yul +//! loops, but the section boundaries are planned here instead of being +//! recomputed ad hoc by templates, the repacker, and `Data`. + +use crate::lowering::{ + layout::{self, G1_BYTES, WORD_BYTES}, + protocol::{CommitmentRead, ProtocolPlan}, +}; + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub(crate) struct ProofSection { + /// Start byte offset in verifier calldata. + pub(crate) start: usize, + /// Number of homogeneous items in this section. + pub(crate) item_count: usize, + /// Byte length of one item. + pub(crate) item_bytes: usize, + /// Total byte length of the section. + pub(crate) byte_len: usize, +} + +impl ProofSection { + /// Construct a homogeneous section and derive its byte length. + fn new(start: usize, item_count: usize, item_bytes: usize) -> Self { + Self { + start, + item_count, + item_bytes, + byte_len: item_count * item_bytes, + } + } + + /// End byte offset, exclusive. + pub(crate) fn end(self) -> usize { + self.start + self.byte_len + } +} + +/// Calldata layout for one lookup argument's commitment reads. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub(crate) struct ProofLookupCommitmentsLayout { + /// Lookup argument index. + pub(crate) lookup: usize, + /// Chunk helper commitments for this lookup. + pub(crate) helpers: ProofSection, + /// LogUp accumulator commitment for this lookup. + pub(crate) accumulator: ProofSection, +} + +/// Complete calldata section map for the generated verifier proof argument. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub(crate) struct ProofCalldataLayout { + /// Byte offset of the first proof payload byte. + pub(crate) proof_cptr: usize, + /// Advice commitments grouped by user phase. + pub(crate) advice_phases: Vec, + /// Lookup multiplicity commitments. + pub(crate) lookup_multiplicities: ProofSection, + /// Permutation product commitments. + pub(crate) permutation_products: ProofSection, + /// Per-lookup helper and accumulator commitments. + pub(crate) lookups: Vec, + /// Trashcan commitments. + pub(crate) trash: ProofSection, + /// Quotient limb commitments. + pub(crate) quotient_limbs: ProofSection, + /// Main scalar evaluation block. + pub(crate) evals: ProofSection, + /// KZG `f_com` commitment. + pub(crate) f_com: ProofSection, + /// KZG point-set evaluation scalars. + pub(crate) q_evals: ProofSection, + /// KZG proof commitment `pi`. + pub(crate) pi: ProofSection, + /// Start of quotient commitment section. + pub(crate) quotient_comm_cptr: usize, + /// Start of the main evaluation scalar section. + pub(crate) eval_cptr: usize, + /// Start of the trailing KZG multi-open G1 block (`f_com`). + pub(crate) w_cptr: usize, + /// Start of quotient point-set evaluation scalars. + pub(crate) q_eval_cptr: usize, + /// End byte offset of the proof payload. + pub(crate) proof_end: usize, + /// Total proof payload byte length. + pub(crate) proof_len: usize, +} + +impl ProofCalldataLayout { + /// Reserve one homogeneous G1 commitment section and assert it matches the + /// logical protocol read order. + fn take_commitment_section( + protocol: &ProtocolPlan, + read_idx: &mut usize, + cursor: &mut usize, + expected: CommitmentRead, + count: usize, + label: &str, + ) -> ProofSection { + let section = ProofSection::new(*cursor, count, G1_BYTES); + for local in 0..count { + let actual = protocol.proof.commitments.get(*read_idx).copied(); + assert_eq!( + actual, + Some(expected), + "proof commitment plan drift at {label}[{local}]: got {actual:?}, expected {expected:?}" + ); + *read_idx += 1; + } + *cursor = section.end(); + section + } + + /// Build the calldata layout by replaying the protocol proof-read order. + /// + /// `num_evals` and `num_point_sets` include feature-dependent dummy evals + /// and PCS point-set planning results, so this function stays independent + /// of those later codegen passes. + pub(crate) fn from_protocol( + protocol: &ProtocolPlan, + proof_cptr: usize, + num_evals: usize, + num_point_sets: usize, + ) -> Self { + let mut cursor = proof_cptr; + let mut read_idx = 0usize; + let word_section = |cursor: &mut usize, item_count: usize| { + let section = ProofSection::new(*cursor, item_count, WORD_BYTES); + *cursor = section.end(); + section + }; + + let advice_phases = protocol + .num_user_advices + .iter() + .copied() + .map(|count| { + Self::take_commitment_section( + protocol, + &mut read_idx, + &mut cursor, + CommitmentRead::Advice, + count, + "advice phase", + ) + }) + .collect::>(); + let lookup_multiplicities = Self::take_commitment_section( + protocol, + &mut read_idx, + &mut cursor, + CommitmentRead::LookupMultiplicity, + protocol.num_lookups, + "lookup multiplicity", + ); + let permutation_products = Self::take_commitment_section( + protocol, + &mut read_idx, + &mut cursor, + CommitmentRead::PermutationProduct, + protocol.num_permutation_zs, + "permutation product", + ); + let lookups = protocol + .lookup_chunks + .iter() + .copied() + .enumerate() + .map(|(lookup, chunks)| ProofLookupCommitmentsLayout { + lookup, + helpers: Self::take_commitment_section( + protocol, + &mut read_idx, + &mut cursor, + CommitmentRead::LookupHelper, + chunks, + "lookup helper", + ), + accumulator: Self::take_commitment_section( + protocol, + &mut read_idx, + &mut cursor, + CommitmentRead::LookupAccumulator, + 1, + "lookup accumulator", + ), + }) + .collect::>(); + let trash = Self::take_commitment_section( + protocol, + &mut read_idx, + &mut cursor, + CommitmentRead::Trash, + protocol.num_trashcans, + "trash", + ); + + let quotient_comm_cptr = cursor; + let quotient_limbs = Self::take_commitment_section( + protocol, + &mut read_idx, + &mut cursor, + CommitmentRead::Quotient, + protocol.num_quotients, + "quotient limb", + ); + assert_eq!( + read_idx, + protocol.proof.commitments.len(), + "proof commitment layout did not consume the full protocol plan" + ); + let eval_cptr = cursor; + let evals = word_section(&mut cursor, num_evals); + let w_cptr = cursor; + let f_com = ProofSection::new(cursor, 1, G1_BYTES); + cursor = f_com.end(); + let q_eval_cptr = cursor; + let q_evals = word_section(&mut cursor, num_point_sets); + let pi = ProofSection::new(cursor, 1, G1_BYTES); + cursor = pi.end(); + let proof_end = cursor; + + Self { + proof_cptr, + advice_phases, + lookup_multiplicities, + permutation_products, + lookups, + trash, + quotient_limbs, + evals, + f_com, + q_evals, + pi, + quotient_comm_cptr, + eval_cptr, + w_cptr, + q_eval_cptr, + proof_end, + proof_len: proof_end - proof_cptr, + } + } + + /// Number of proof G1 commitments before quotient limbs. + pub(crate) fn non_quotient_g1_count(&self) -> usize { + self.advice_phases.iter().map(|section| section.item_count).sum::() + + self.lookup_multiplicities.item_count + + self.permutation_products.item_count + + self + .lookups + .iter() + .map(|lookup| lookup.helpers.item_count + lookup.accumulator.item_count) + .sum::() + + self.trash.item_count + } + + /// Number of proof G1 commitments that participate in transcript reads. + pub(crate) fn commitment_g1_count(&self) -> usize { + self.non_quotient_g1_count() + self.quotient_limbs.item_count + } + + /// Total number of G1 points in the Solidity-facing proof payload. + pub(crate) fn total_g1_count(&self) -> usize { + self.commitment_g1_count() + self.f_com.item_count + self.pi.item_count + } + + /// Commitment group sizes in the exact read/repack order. + pub(crate) fn commitment_read_groups(&self) -> Vec { + self.advice_phases + .iter() + .map(|section| section.item_count) + .filter(|&count| count != 0) + .chain( + [ + self.lookup_multiplicities.item_count, + self.permutation_products.item_count, + ] + .into_iter() + .filter(|&count| count != 0), + ) + .chain( + self.lookups + .iter() + .flat_map(|lookup| [lookup.helpers.item_count, lookup.accumulator.item_count]), + ) + .chain((self.trash.item_count != 0).then_some(self.trash.item_count)) + .chain(std::iter::once(self.quotient_limbs.item_count)) + .collect() + } +} + +/// Conservative bound for the streaming transcript buffer. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub(crate) struct TranscriptBufferLayout { + /// Max bytes before the first challenge squeeze. + pub(crate) initial_run_bytes: usize, + /// Max bytes for the evaluation scalar absorb run. + pub(crate) eval_run_bytes: usize, + /// Max bytes for the full commitment/scalar absorb run. + pub(crate) total_run_bytes: usize, + /// Required EVM words above `TRANSCRIPT_BUFFER_START`. + pub(crate) words: usize, +} + +impl TranscriptBufferLayout { + /// Derive transcript-buffer bounds from proof calldata shape and instances. + pub(crate) fn from_proof_layout(proof: &ProofCalldataLayout, num_instances: usize) -> Self { + let word_absorb = layout::transcript::WORD_ABSORB_BYTES; + let g1_absorb = layout::transcript::G1_ABSORB_BYTES; + let squeeze_cushion = layout::transcript::POST_SQUEEZE_CUSHION_WORDS * WORD_BYTES; + + let phase_1_advices = + proof.advice_phases.first().map(|section| section.item_count).unwrap_or(0); + let initial_run_bytes = word_absorb + + g1_absorb + + word_absorb + + num_instances * word_absorb + + phase_1_advices * g1_absorb + + squeeze_cushion; + + let eval_run_bytes = proof.quotient_limbs.byte_len + + proof.evals.byte_len + + proof.q_evals.byte_len + + squeeze_cushion; + + let total_scalar_bytes = proof.evals.byte_len + + proof.q_evals.byte_len + + layout::transcript::POST_SQUEEZE_CUSHION_WORDS * word_absorb; + let total_run_bytes = + word_absorb + proof.total_g1_count() * g1_absorb + total_scalar_bytes + squeeze_cushion; + + let max_bytes = initial_run_bytes.max(eval_run_bytes).max(total_run_bytes); + Self { + initial_run_bytes, + eval_run_bytes, + total_run_bytes, + words: max_bytes.div_ceil(WORD_BYTES), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::lowering::protocol::{CommitmentRead, ProofReadPlan, ProtocolPlan}; + + /// Build a synthetic proof-read plan for calldata layout tests. + fn protocol_shape( + user_advices: Vec, + lookup_chunks: Vec, + permutation_zs: usize, + trashcans: usize, + quotients: usize, + ) -> ProtocolPlan { + let num_lookups = lookup_chunks.len(); + let mut proof = ProofReadPlan::default(); + proof + .commitments + .extend((0..user_advices.iter().sum::()).map(|_| CommitmentRead::Advice)); + proof + .commitments + .extend((0..num_lookups).map(|_| CommitmentRead::LookupMultiplicity)); + proof + .commitments + .extend((0..permutation_zs).map(|_| CommitmentRead::PermutationProduct)); + for chunks in lookup_chunks.iter().copied() { + proof.commitments.extend((0..chunks).map(|_| CommitmentRead::LookupHelper)); + proof.commitments.push(CommitmentRead::LookupAccumulator); + } + proof.commitments.extend((0..trashcans).map(|_| CommitmentRead::Trash)); + proof.commitments.extend((0..quotients).map(|_| CommitmentRead::Quotient)); + + ProtocolPlan { + num_user_advices: user_advices, + advice_indices: (0..proof + .commitments + .iter() + .filter(|read| matches!(read, CommitmentRead::Advice)) + .count()) + .collect(), + lookup_chunks, + num_lookups, + num_permutation_zs: permutation_zs, + num_trashcans: trashcans, + num_quotients: quotients, + proof, + ..ProtocolPlan::default() + } + } + + #[test] + fn proof_layout_matches_legacy_offsets() { + let protocol = protocol_shape(vec![2, 1], vec![2, 0], 1, 1, 3); + let proof_cptr = layout::abi::VERIFY_PROOF_PROOF_CPTR; + let layout = ProofCalldataLayout::from_protocol(&protocol, proof_cptr, 5, 2); + + let non_quotient_g1s = 3 + 2 + 1 + 4 + 1; + assert_eq!(layout.non_quotient_g1_count(), non_quotient_g1s); + assert_eq!( + layout.quotient_comm_cptr, + proof_cptr + non_quotient_g1s * G1_BYTES + ); + assert_eq!(layout.eval_cptr, layout.quotient_comm_cptr + 3 * G1_BYTES); + assert_eq!(layout.w_cptr, layout.eval_cptr + 5 * WORD_BYTES); + assert_eq!(layout.q_eval_cptr, layout.w_cptr + G1_BYTES); + assert_eq!( + layout.proof_len, + (non_quotient_g1s + 3 + 2) * G1_BYTES + (5 + 2) * WORD_BYTES + ); + } + + #[test] + fn proof_layout_preserves_lookup_helper_accumulator_grouping() { + let protocol = protocol_shape(vec![1], vec![2, 1], 0, 0, 2); + let layout = ProofCalldataLayout::from_protocol(&protocol, 0, 0, 0); + + assert_eq!(layout.lookups.len(), 2); + assert_eq!(layout.lookups[0].helpers.item_count, 2); + assert_eq!(layout.lookups[0].accumulator.item_count, 1); + assert_eq!(layout.lookups[0].helpers.start, 3 * G1_BYTES); + assert_eq!(layout.lookups[0].accumulator.start, 5 * G1_BYTES); + assert_eq!(layout.lookups[1].helpers.item_count, 1); + assert_eq!(layout.lookups[1].helpers.start, 6 * G1_BYTES); + assert_eq!(layout.lookups[1].accumulator.start, 7 * G1_BYTES); + } + + #[test] + #[should_panic(expected = "proof commitment plan drift")] + fn proof_layout_rejects_commitment_order_drift() { + let mut protocol = protocol_shape(vec![1], vec![1], 1, 0, 1); + let lhs = protocol + .proof + .commitments + .iter() + .position(|read| matches!(read, CommitmentRead::LookupMultiplicity)) + .expect("lookup multiplicity"); + let rhs = protocol + .proof + .commitments + .iter() + .position(|read| matches!(read, CommitmentRead::PermutationProduct)) + .expect("permutation product"); + protocol.proof.commitments.swap(lhs, rhs); + + let _ = ProofCalldataLayout::from_protocol(&protocol, 0, 0, 0); + } + + #[test] + fn proof_layout_handles_zero_lookup_and_trash() { + let protocol = protocol_shape(vec![0, 4], vec![], 0, 0, 1); + let layout = ProofCalldataLayout::from_protocol(&protocol, 0x64, 7, 0); + + assert_eq!(layout.lookup_multiplicities.byte_len, 0); + assert_eq!(layout.permutation_products.byte_len, 0); + assert_eq!(layout.trash.byte_len, 0); + assert_eq!(layout.quotient_comm_cptr, 0x64 + 4 * G1_BYTES); + assert_eq!(layout.commitment_read_groups(), vec![4, 1]); + } + + #[test] + fn transcript_layout_matches_current_conservative_bound() { + let protocol = protocol_shape(vec![64], vec![], 0, 0, 2); + let proof = ProofCalldataLayout::from_protocol(&protocol, 0, 10, 3); + let transcript = TranscriptBufferLayout::from_proof_layout(&proof, 0); + + let first_phase_run = 32 + 128 + 32 + 64 * 128 + 32 * 32; + assert!(transcript.words * WORD_BYTES >= first_phase_run); + assert_eq!( + transcript.eval_run_bytes, + 2 * G1_BYTES + (10 + 3) * WORD_BYTES + 32 * WORD_BYTES + ); + } +} diff --git a/proofs/solidity-verifier/src/lowering/artifacts.rs b/proofs/solidity-verifier/src/lowering/artifacts.rs new file mode 100644 index 000000000..8d26797d4 --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/artifacts.rs @@ -0,0 +1,299 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Verifier artifact construction for a concrete verifier build. +//! +//! This slice assembles Askama models for the embedded verifier, split verifier +//! plus VK payload, and optional external quotient evaluator after all protocol +//! and layout planning has converged. + +use ruint::aliases::U256; +use sha3::{Digest, Keccak256}; + +use crate::lowering::{ + encoding::Ptr, + kzg, layout, + layout::memory::{G1_BYTES, WORD_BYTES}, + plan::LoweringPlan, + quotient_numerator::vm::{fr_delta_literal, LIMB7_YUL_COEFFS, WIDE_LIMB7_YUL_COEFFS}, + render::{Halo2QuotientEvaluator, Halo2Verifier, UserPhase, VerifierCodegenLayout}, + VerifierBuildInputs, +}; + +impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { + /// Build the Askama model for the standalone quotient evaluator contract + /// from an already-converged lowering plan. + pub(crate) fn generate_quotient_evaluator_from_plan( + &self, + plan: &LoweringPlan, + trace: bool, + ) -> Halo2QuotientEvaluator { + let quotient_rendering = plan.quotient_evaluator_rendering(self, trace); + let quotient_helper_flags = quotient_rendering.blocks.helper_flags(); + + let quotient_evaluator = Halo2QuotientEvaluator { + template_constants: Default::default(), + trace, + quotient_pow5_helper: quotient_helper_flags.pow5, + quotient_limb7_helper: quotient_helper_flags.limb7, + quotient_wide_limb7_helper: quotient_helper_flags.wide_limb7, + limb7_yul_coeffs: LIMB7_YUL_COEFFS, + wide_limb7_yul_coeffs: WIDE_LIMB7_YUL_COEFFS, + fr_delta: fr_delta_literal(), + memory: plan.memory.clone(), + vk_mptr: plan.vk_mptr, + vk_len: plan.vk.len(), + challenge_mptr: plan.data.challenge_mptr, + num_user_challenges: plan.meta.num_user_challenges.iter().sum(), + theta_mptr: plan.data.theta_mptr, + reversed_evals_mptr: plan.data.reversed_evals_mptr, + num_evals: plan.meta.num_evals, + quotient_external: quotient_rendering.external, + quotient_inline_computations: quotient_rendering.blocks.inline_computations, + quotient_eval_numer_computations: quotient_rendering.blocks.eval_numer_computations, + quotient_post_vm_computations: quotient_rendering.blocks.post_vm_computations, + quotient_native_permutation_computation: quotient_rendering + .blocks + .native_permutation_computation, + quotient_native_lookup_computation: quotient_rendering.blocks.native_lookup_computation, + quotient_native_identity_computations: quotient_rendering + .blocks + .native_identity_computations, + quotient_program: Some(quotient_rendering.program), + simple_selector_cols: plan.quotient.sorted_simple.clone(), + quotient_identity_trace_base: layout::trace::QUOTIENT_IDENTITY_BASE, + }; + quotient_evaluator + .validate_layout() + .unwrap_or_else(|err| panic!("invalid generated quotient evaluator layout: {err}")); + quotient_evaluator + } + + /// Build the Askama model for the main Solidity verifier contract from an + /// already-converged lowering plan. + pub(crate) fn generate_verifier_from_plan( + &self, + plan: &LoweringPlan, + separate: bool, + trace: bool, + gas_checkpoints: bool, + external_quotient: bool, + expected_quotient: Option<(usize, U256)>, + ) -> Halo2Verifier { + assert!( + expected_quotient.is_none() || external_quotient, + "quotient pinning requires an external quotient evaluator" + ); + assert!( + !external_quotient || expected_quotient.is_some(), + "external quotient evaluator render requires a generated runtime length/codehash; \ + render the quotient evaluator first and use the pinned quotient render API" + ); + let meta = &plan.meta; + let data = &plan.data; + let proof_layout = &plan.proof_layout; + let proof_cptr = Ptr::calldata(proof_layout.proof_cptr); + let memory = plan.memory.clone(); + let vk_mptr = plan.vk_mptr; + let sorted_simple = &plan.quotient.sorted_simple; + + let selector_acc_mptr = plan.memory.selector_acc_mptr; + let expected_vk_codehash = separate.then(|| { + let digest: [u8; 32] = Keccak256::digest(plan.vk.runtime_bytes()).into(); + U256::from_be_bytes(digest) + }); + let vk_len = plan.vk.len(); + let (expected_quotient_len, expected_quotient_codehash) = expected_quotient + .map(|(len, codehash)| (Some(len), Some(codehash))) + .unwrap_or((None, None)); + let quotient_rendering = plan.quotient_rendering(self, trace, external_quotient); + let quotient_helper_flags = quotient_rendering.helper_flags(); + let (quotient_external, quotient_blocks, quotient_program) = + quotient_rendering.into_template_parts(); + + let pcs_computations = kzg::computations( + &plan.meta, + &plan.data, + &plan.memory, + cfg!(feature = "truncated-challenges"), + trace, + ); + + // Per-user-phase breakdown (advices + user challenges). + let user_phases: Vec = meta + .num_user_advices + .iter() + .zip(meta.num_user_challenges.iter()) + .enumerate() + .map(|(idx, (_, &n_c))| UserPhase { + advice_bytes: proof_layout.advice_phases[idx].byte_len, + num_challenges: n_c, + challenge_offset: meta.num_user_challenges.iter().take(idx).sum(), + }) + .collect(); + let num_user_challenges: usize = meta.num_user_challenges.iter().sum(); + let lookup_h_plus_acc: usize = meta.lookup_chunks.iter().sum::() + meta.num_lookups; + + // Compute VK / accumulator layout helpers before moving vk into + // the template struct. + let fixed_comm_mptr_byte = (vk_mptr + plan.vk.constants.len()).value().as_usize(); + let permutation_comm_mptr_byte = + fixed_comm_mptr_byte + plan.vk.fixed_comms.len() * G1_BYTES; + let g1_base_mptr_byte = (vk_mptr + layout::VkHeaderSlot::G1Base.word()).value().as_usize(); + + let mut acc_fixed_bases: Vec<(String, usize, bool)> = self + .acc_encoding + .map(|acc_encoding| { + let fixed_scalar_count = acc_encoding + .fixed_scalar_count(self.num_instances) + .expect("accumulator encoding validated by GeneratorConfig"); + // A fully-collapsed public accumulator has no fixed-base scalar + // tail: just (lhs point, lhs scalar=1, rhs point, rhs scalar=1). + // Older partially-collapsed accumulators still expose the RHS + // fixed scalars for -G, fixed commitments, and permutation + // commitments in BTreeMap key order. + let bases = if fixed_scalar_count == 0 { + Vec::new() + } else { + let num_perm_bases = plan.vk.permutation_comms.len(); + let num_fixed_bases = fixed_scalar_count + .checked_sub(1 + num_perm_bases) + .expect("accumulator fixed scalar count is smaller than -G + permutations"); + + std::iter::once(("-G".to_string(), g1_base_mptr_byte, true)) + .chain((0..num_fixed_bases).map(|i| { + ( + format!("self_vk_fixed_com_{i}"), + fixed_comm_mptr_byte + i * G1_BYTES, + false, + ) + })) + .chain((0..num_perm_bases).map(|i| { + ( + format!("self_vk_perm_com_{i}"), + permutation_comm_mptr_byte + i * G1_BYTES, + false, + ) + })) + .collect::>() + }; + debug_assert_eq!( + bases.len(), + fixed_scalar_count, + "accumulator fixed-base scalar tail must match generated bases" + ); + bases + }) + .unwrap_or_default(); + acc_fixed_bases.sort_by(|a, b| a.0.cmp(&b.0)); + let acc_fixed_bases: Vec<(usize, bool)> = + acc_fixed_bases.into_iter().map(|(_, mptr, negate)| (mptr, negate)).collect(); + let ( + expected_has_accumulator, + expected_acc_offset, + expected_num_acc_limbs, + expected_num_acc_limb_bits, + expected_acc_has_carried_scalars, + ) = self + .acc_encoding + .map(|acc_encoding| { + ( + true, + acc_encoding.offset, + acc_encoding.num_limbs, + acc_encoding.num_limb_bits, + acc_encoding.has_carried_scalars(), + ) + }) + .unwrap_or((false, 0, 0, 0, false)); + + let g1msm_single_gas_cap = layout::precompile::g1msm_gas_cap(layout::G1_MSM_PAIR_BYTES); + let lin_trace_g1msm_gas_cap = layout::precompile::g1msm_gas_cap( + (meta.num_quotients + sorted_simple.len()) * layout::G1_MSM_PAIR_BYTES, + ); + // The accumulator RHS MSM always includes the carried RHS point and may + // append generated fixed bases whose public scalars are nonzero. A max + // cap is safe because unused precompile gas is returned, and it avoids + // rendering the EIP-2537 discount-table switch into runtime bytecode. + let acc_rhs_g1msm_gas_cap = layout::precompile::g1msm_gas_cap( + (1 + acc_fixed_bases.len()) * layout::G1_MSM_PAIR_BYTES, + ); + let final_pairing_gas_cap = + layout::precompile::pairing_gas_cap(layout::PAIRING_TWO_PAIR_BYTES); + + let verifier = Halo2Verifier { + template_constants: Default::default(), + trace, + gas_checkpoints, + quotient_pow5_helper: quotient_helper_flags.pow5, + quotient_limb7_helper: quotient_helper_flags.limb7, + quotient_wide_limb7_helper: quotient_helper_flags.wide_limb7, + g1msm_single_gas_cap, + lin_trace_g1msm_gas_cap, + acc_rhs_g1msm_gas_cap, + final_pairing_gas_cap, + limb7_yul_coeffs: LIMB7_YUL_COEFFS, + wide_limb7_yul_coeffs: WIDE_LIMB7_YUL_COEFFS, + fr_delta: fr_delta_literal(), + embedded_vk: (!separate).then(|| plan.vk.clone()), + expected_vk_codehash, + vk_len, + num_instances: self.num_instances, + k: self.vk.get_domain().k() as usize, + codegen_layout: VerifierCodegenLayout { + proof: plan.proof_layout.clone(), + }, + memory, + vk_header: Default::default(), + vk_mptr, + num_neg_lagranges: meta.rotation_last.unsigned_abs() as usize, + user_phases, + num_user_challenges, + num_lookups: meta.num_lookups, + num_permutation_zs: meta.num_permutation_zs, + lookup_h_plus_acc, + num_trashcans: meta.num_trashcans, + num_quotients: meta.num_quotients, + num_evals: meta.num_evals, + comms_mptr_base: data.comms_mptr_base, + reversed_evals_mptr: data.reversed_evals_mptr, + selector_acc_mptr, + quotient_external, + expected_quotient_len, + expected_quotient_codehash, + proof_cptr, + abi_selector_bytes: layout::abi::SELECTOR_BYTES, + abi_proof_head_offset: layout::abi::VERIFY_PROOF_PROOF_HEAD_OFFSET, + abi_instances_head_cptr: layout::abi::SELECTOR_BYTES + WORD_BYTES, + num_instance_cptr: proof_layout.proof_end, + instance_cptr: proof_layout.proof_end + WORD_BYTES, + quotient_comm_cptr: Ptr::calldata(proof_layout.quotient_comm_cptr), + proof_len: proof_layout.proof_len, + challenge_mptr: data.challenge_mptr, + theta_mptr: data.theta_mptr, + quotient_inline_computations: quotient_blocks.inline_computations, + quotient_eval_numer_computations: quotient_blocks.eval_numer_computations, + quotient_post_vm_computations: quotient_blocks.post_vm_computations, + quotient_native_permutation_computation: quotient_blocks.native_permutation_computation, + quotient_native_lookup_computation: quotient_blocks.native_lookup_computation, + quotient_native_identity_computations: quotient_blocks.native_identity_computations, + quotient_program, + pcs_computations, + simple_selector_cols: sorted_simple.clone(), + proof_commit_trace_base: layout::trace::PROOF_COMMIT_BASE, + proof_eval_trace_base: layout::trace::PROOF_EVAL_BASE, + quotient_identity_trace_base: layout::trace::QUOTIENT_IDENTITY_BASE, + selector_trace_base: layout::trace::SELECTOR_FOLD_BASE, + fixed_comm_mptr: fixed_comm_mptr_byte, + truncated_challenges: cfg!(feature = "truncated-challenges"), + expected_has_accumulator, + expected_acc_offset, + expected_num_acc_limbs, + expected_num_acc_limb_bits, + expected_acc_has_carried_scalars, + acc_fixed_bases, + }; + verifier + .validate_layout() + .unwrap_or_else(|err| panic!("invalid generated verifier layout: {err}")); + verifier + } +} diff --git a/proofs/solidity-verifier/src/lowering/calldata.rs b/proofs/solidity-verifier/src/lowering/calldata.rs new file mode 100644 index 000000000..0a3a462bc --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/calldata.rs @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Proof repacking for the Solidity-facing ABI. +//! +//! Midnight proof bytes keep compressed G1 commitments and little-endian +//! scalars; generated Solidity expects EIP-2537 padded G1 words and +//! big-endian scalar words. This module performs that deterministic boundary +//! conversion. + +use group::GroupEncoding; +use midnight_curves::{Fq, G1Affine}; + +#[cfg(all(test, feature = "evm"))] +use crate::lowering::quotient_numerator::vm::RepackedProofScalarLayout; +use crate::{ + api::{GeneratorError, RepackError}, + lowering::{ + layout, + quotient_numerator::vm::{scalar_le_to_be_word, RepackedProofLayoutPlan}, + VerifierBuildInputs, + }, +}; + +impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { + /// Repack a midnight-proofs proof from the on-the-wire compressed + /// form (each G1 = 48 bytes ZCash compressed) into the EIP-2537 + /// padded form (each G1 = 4 × 32-byte BE words) and rewrites scalar + /// proof elements from midnight-proofs' canonical LE bytes into + /// canonical BE calldata words. This is the Solidity-facing proof + /// shim; the native proof bytes remain unchanged. + /// + /// This is the off-chain repack step the verifier expects: the + /// EVM does **not** run the modexp-based BLS12-381 decompression + /// at every G1 site (which would cost ~80 kg / commitment); the + /// caller pays that cost off-chain once. + /// + /// The walk is driven entirely by the bound `&self.vk` / + /// `&self.meta` (so it correctly handles the non-trivial proof + /// shapes the codegen produces: per-phase advices, lookup + /// multiplicities + chunked helpers + accumulators, perm Z + /// products, trashcans, quotient limbs, eval block, dummy evals + /// from `outer-fewer-point-sets`, f_com, q_evals per point set, pi). + pub(crate) fn repack_proof(&self, compressed: &[u8]) -> Result, RepackError> { + use group::prime::PrimeCurveAffine; + + let plan = self.repacked_proof_layout_plan(); + let prefix_g1_count = plan.prefix_g1_count(); + let expected_compressed_len = plan.compressed_len(); + if compressed.len() != expected_compressed_len { + return Err(RepackError::LengthMismatch { + expected: expected_compressed_len, + actual: compressed.len(), + prefix_g1: prefix_g1_count, + num_evals: plan.num_evals, + num_point_sets: plan.num_point_sets, + }); + } + + let mut out: Vec = Vec::with_capacity(plan.repacked_len()); + let mut cursor = 0usize; + let push_g1 = |cursor: &mut usize, out: &mut Vec| -> Result<(), RepackError> { + let mut comp = ::Repr::default(); + comp.as_mut() + .copy_from_slice(&compressed[*cursor..*cursor + layout::G1_COMPRESSED_BYTES]); + let cur = *cursor; + *cursor += layout::G1_COMPRESSED_BYTES; + let Some(pt) = Option::::from(::from_bytes(&comp)) + else { + return Err(RepackError::InvalidCompressedG1 { + offset: cur, + bytes_hex: hex::encode(comp.as_ref()), + }); + }; + if bool::from(pt.is_identity()) { + out.extend_from_slice(&[0u8; 128]); + return Ok(()); + } + let x_be = pt.x().to_bytes_be(); + let y_be = pt.y().to_bytes_be(); + out.extend_from_slice(&[0u8; 16]); + out.extend_from_slice(&x_be[0..16]); + out.extend_from_slice(&x_be[16..48]); + out.extend_from_slice(&[0u8; 16]); + out.extend_from_slice(&y_be[0..16]); + out.extend_from_slice(&y_be[16..48]); + Ok(()) + }; + let push_scalar_be = |cursor: &mut usize, out: &mut Vec| { + out.extend_from_slice(&scalar_le_to_be_word( + &compressed[*cursor..*cursor + layout::WORD_BYTES], + )); + *cursor += layout::WORD_BYTES; + }; + for &n in &plan.g1_groups { + for _ in 0..n { + push_g1(&mut cursor, &mut out)?; + } + } + // evals (Fr 32-byte LE in native proof) -> BE calldata words + // (incl. dummy slots). + for _ in 0..plan.num_evals { + push_scalar_be(&mut cursor, &mut out); + } + // f_com + push_g1(&mut cursor, &mut out)?; + // q_evals (Fr 32-byte LE in native proof) -> BE calldata words. + for _ in 0..plan.num_point_sets { + push_scalar_be(&mut cursor, &mut out); + } + // pi + push_g1(&mut cursor, &mut out)?; + debug_assert_eq!(cursor, compressed.len()); + Ok(out) + } + + /// Repack a native proof and encode verifier calldata for `verifyProof`. + pub(crate) fn encode_calldata( + &self, + compressed_proof: &[u8], + instances: &[Fq], + ) -> Result, GeneratorError> { + if instances.len() != self.num_instances { + return Err(GeneratorError::Planning { + stage: "calldata", + message: format!( + "instance length mismatch: expected {}, got {}", + self.num_instances, + instances.len() + ), + }); + } + let proof = self.repack_proof(compressed_proof)?; + Ok(crate::evm::encode_calldata(&proof, instances)) + } + + #[cfg(all(test, feature = "evm"))] + pub(crate) fn repacked_proof_scalar_layout_for_test(&self) -> RepackedProofScalarLayout { + self.repacked_proof_layout_plan().scalar_layout() + } + + /// Return the compressed/repacked proof layout used by the off-chain shim. + fn repacked_proof_layout_plan(&self) -> RepackedProofLayoutPlan { + self.lowering_plan().repacked_proof_layout_plan() + } +} diff --git a/proofs/solidity-verifier/src/lowering/config.rs b/proofs/solidity-verifier/src/lowering/config.rs new file mode 100644 index 000000000..7be24e5c5 --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/config.rs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Constants for the generated verifier shape used by the IVC Solidity bench. +//! +//! The generator intentionally emits one shape: a compact byte-oriented +//! quotient VM with a small direct prefix, selected native callbacks, native +//! permutation/lookup families, structured trash suffix, and limb-aware VM +//! opcodes. + +// Keep a small direct prefix as the default gas-capped compact VM path: +// it avoids running the entire numerator through the interpreter while staying +// below the external quotient evaluator size budget. +pub(crate) const DEFAULT_HYBRID_QUOTIENT_INLINE_IDENTITIES: usize = 4; + +// Spend a bounded slice of quotient-evaluator bytecode headroom on native VM +// callbacks. After the direct prefix, up to N remaining gate identities are +// emitted as VM opcodes that call generated Yul blocks; everything else stays +// in the compact interpreter. Selection is a gas/byte knapsack under an +// estimated native callback byte budget. +pub(crate) const DEFAULT_QUOTIENT_NATIVE_GATES: usize = 4; +// The compact quotient VM path is the default size-oriented emitter: it stores +// identity arithmetic as data in the VK and interprets it from one small Yul +// loop. Emit the final trash suffix as structured Yul by default to save +// dispatch gas. +// Limb-aware VM superinstructions are part of the default byte-oriented +// pinned-VK lowering: they structurally compress recurring 7-limb +// foreign-field expressions while preserving the compact quotient interpreter. +pub(crate) const DEFAULT_QUOTIENT_LIMB_VM_OPS: bool = true; diff --git a/proofs/solidity-verifier/src/lowering/diagnostics.rs b/proofs/solidity-verifier/src/lowering/diagnostics.rs new file mode 100644 index 000000000..48364bbd2 --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/diagnostics.rs @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Host-side diagnostics derived from the same lowering plan used for +//! rendering. + +use crate::{ + api::{ + ProofEvaluationCounts, QuotientIdentityManifest, QuotientIdentityManifestEntry, + QuotientIdentityManifestTarget, QuotientIdentitySource, + }, + lowering::{protocol, VerifierBuildInputs}, +}; + +/// Count proof-evaluation scalars in the same categories used by calldata +/// parsing and transcript absorption. +pub(crate) fn proof_evaluation_counts( + inputs: VerifierBuildInputs<'_, '_>, +) -> ProofEvaluationCounts { + debug_assert_eq!( + inputs.num_committed_instances, + inputs.meta.num_committed_instances + ); + let plan = inputs.lowering_plan(); + let meta = &plan.meta; + + let committed_instance = meta + .instance_queries + .iter() + .filter(|(col, _)| *col < meta.num_committed_instances) + .count(); + let computed_instance = meta.instance_queries.len() - committed_instance; + let permutation_product = if meta.num_permutation_zs == 0 { + 0 + } else { + 3 * meta.num_permutation_zs - 1 + }; + let lookup_helper = meta.lookup_chunks.iter().sum(); + + let counts = ProofEvaluationCounts { + committed_instance, + computed_instance, + advice: meta.advice_queries.len(), + // Fixed evals are query-based proof reads. The Rust verifier + // reads the non-simple fixed queries that appear in + // `protocol.proof.evals`; simple selector columns are synthesized + // locally and feed selector buckets instead of proof scalars. + fixed: meta + .protocol + .proof + .evals + .iter() + .filter(|eval| matches!(eval, protocol::EvalRead::Fixed(_))) + .count(), + simple_selector_fixed: meta.num_simple_selectors, + permutation_common: meta.permutation_columns.len(), + permutation_product, + permutation_sets: meta.num_permutation_zs, + lookup_multiplicity: meta.num_lookups, + lookup_helper, + lookup_accumulator: 2 * meta.num_lookups, + trash: meta.num_trashcans, + dummy: meta.num_dummy_evals, + }; + + assert_eq!( + counts.proof_total(), + meta.num_evals, + "proof evaluation count accounting must match verifier proof layout" + ); + counts +} + +/// Build a stable source-indexed manifest for the generated quotient identity +/// stream. +pub(crate) fn quotient_identity_manifest( + inputs: VerifierBuildInputs<'_, '_>, +) -> QuotientIdentityManifest { + let plan = inputs.lowering_plan(); + let mut simple_selector_cols: Vec = + plan.meta.simple_selector_cols.iter().copied().collect(); + simple_selector_cols.sort_unstable(); + + let gate_entries = inputs + .vk + .cs() + .gates() + .iter() + .enumerate() + .flat_map(|(gate_index, gate)| { + let target = gate + .queried_selectors() + .iter() + .find(|selector| selector.is_simple()) + .map(|selector| { + let selector_index = simple_selector_cols + .iter() + .position(|fixed| *fixed == selector.index()) + .expect("simple selector fixed column present"); + QuotientIdentityManifestTarget::Selector { + selector_index, + fixed_column: selector.index(), + } + }) + .unwrap_or(QuotientIdentityManifestTarget::Main); + + (0..gate.polynomials().len()).map(move |polynomial_index| { + ( + gate_index, + gate.name().to_string(), + polynomial_index, + gate.constraint_name(polynomial_index).to_string(), + target, + ) + }) + }) + .enumerate() + .map( + |(global_index, (gate_index, gate_name, polynomial_index, constraint_name, target))| { + QuotientIdentityManifestEntry { + global_index, + source: QuotientIdentitySource::Gate { + gate_index, + gate_name, + constraint_index: polynomial_index, + constraint_name, + polynomial_index, + }, + target, + } + }, + ) + .collect::>(); + let permutation_base = gate_entries.len(); + let lookup_base = permutation_base + plan.meta.protocol.quotient.permutation; + let trash_base = lookup_base + plan.meta.protocol.quotient.lookup; + + let entries = gate_entries + .into_iter() + .chain( + (0..plan.meta.protocol.quotient.permutation).map(|identity_index| { + QuotientIdentityManifestEntry { + global_index: permutation_base + identity_index, + source: QuotientIdentitySource::Permutation { identity_index }, + target: QuotientIdentityManifestTarget::Main, + } + }), + ) + .chain( + (0..plan.meta.protocol.quotient.lookup).map(|identity_index| { + let (lookup_index, _) = plan + .meta + .protocol + .lookup_identity_source(identity_index) + .expect("lookup identity index covered by protocol lookup chunks"); + let lookup_name = format!("lookup_{lookup_index}"); + QuotientIdentityManifestEntry { + global_index: lookup_base + identity_index, + source: QuotientIdentitySource::Lookup { + identity_index, + lookup_index, + lookup_name, + }, + target: QuotientIdentityManifestTarget::Main, + } + }), + ) + .chain((0..plan.meta.protocol.quotient.trash).map(|trash_index| { + let trash_name = inputs.trash_manifest_name(trash_index); + QuotientIdentityManifestEntry { + global_index: trash_base + trash_index, + source: QuotientIdentitySource::Trash { + trash_index, + trash_name, + }, + target: QuotientIdentityManifestTarget::Main, + } + })) + .collect(); + + QuotientIdentityManifest { + entries, + gate_identities: plan.meta.protocol.quotient.gates, + permutation_identities: plan.meta.protocol.quotient.permutation, + lookup_identities: plan.meta.protocol.quotient.lookup, + trash_identities: plan.meta.protocol.quotient.trash, + simple_selector_cols, + } +} + +impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { + /// Best-effort human label for a trash identity. + /// + /// Empty-polynomial gates are the usual source because trash arguments are + /// created from additive selectors. If the gate metadata is unavailable, + /// fall back to the constraint-system trash list and finally to a stable + /// synthetic name. + pub(crate) fn trash_manifest_name(&self, trash_index: usize) -> String { + if let Some(gate) = self + .vk + .cs() + .gates() + .iter() + .filter(|gate| gate.polynomials().is_empty()) + .nth(trash_index) + { + return gate.name().to_string(); + } + + self.vk + .cs() + .trashcans() + .get(trash_index) + .map(|trash| trash.name().to_string()) + .unwrap_or_else(|| format!("trash_{trash_index}")) + } +} diff --git a/proofs/solidity-verifier/src/lowering/encoding/mod.rs b/proofs/solidity-verifier/src/lowering/encoding/mod.rs new file mode 100644 index 000000000..5ddf9bcdf --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/encoding/mod.rs @@ -0,0 +1,921 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Shared metadata, pointer, formatting, and BLS encoding helpers for codegen. +//! +//! This module is intentionally close to the generated Yul model. `Ptr`, +//! `Word`, and `EcPoint` render directly as calldata/memory loads, while +//! `ConstraintSystemMeta` and `Data` bind the typed protocol plan to concrete +//! verifier memory addresses. +use std::{ + borrow::Borrow, + collections::{BTreeSet, HashMap}, + fmt::{self, Display, Formatter}, + ops::{Add, Sub}, +}; + +use ff::PrimeField; +use itertools::{izip, Itertools}; +use midnight_curves::{Coordinates, CurveAffine, Fq, G1Affine, G2Affine}; +use midnight_proofs::plonk::{Any, Column, ConstraintSystem}; +use ruint::{aliases::U256, UintTryFrom}; + +use crate::lowering::{ + layout::{ + memory::{VerifierMemoryLayout, G1_WORDS, WORD_BYTES}, + BLS_FP_BYTES, EIP2537_FP_PAD_BYTES, + }, + protocol::{EvalRead, PermutationZEval, ProtocolPlan}, + render::Halo2VerifyingKey, +}; + +/// Planned lookup eval slots: multiplicity, helpers, accumulator current, and +/// accumulator next. +type LookupEvalSlots = (Option, Vec>, Option, Option); + +// ---------------------------------------------------------------------------- +// `ConstraintSystemMeta` walks `midnight_proofs::plonk::ConstraintSystem` +// directly, which exposes: +// +// * `cs.lookups()` -> `Vec>` (with +// `chunk_by_degree`, `num_chunks`) +// * `cs.trashcans()` -> `Vec>` +// * `cs.permutation()` -> `&permutation::Argument` with `get_columns()` +// * `cs.num_simple_selectors()`, `cs.has_simple_selector_col(idx)` +// +// The proof byte layout this metadata describes corresponds to +// `midnight_proofs::plonk::verifier::parse_trace`: +// per phase: read advices, squeeze challenges, ... +// theta +// per proof: read multiplicities (one G1 per lookup) +// beta, gamma +// per proof: read permutation product commitments +// per proof: per lookup, read num_chunks helpers + 1 accumulator +// trash_challenge +// per proof: read trashcan commitments +// y +// read quotient limbs +// x +// read evaluations (committed_instance? + advice + fixed-non-simple + +// perm_common + perm_set + lookup + trash) +// PCS (multi_prepare): +// x1, x2; read f_com; x3; read q_evals (one per point set); +// x4; read pi +// +// Steps 4-9 of docs/architecture/MIGRATION.md track the remaining work to +// materialise this schema into a complete Yul emitter. For Steps 1-3 we only +// need the scalar metadata fields below to be derivable from the new CS so that +// `cargo check --lib` is green. +// ---------------------------------------------------------------------------- + +#[derive(Debug, Clone, Default)] +pub(crate) struct ConstraintSystemMeta { + /// Typed source plan for proof reads and PCS queries. + pub(crate) protocol: ProtocolPlan, + /// Number of fixed columns. + pub(crate) num_fixeds: usize, + /// Columns participating in the permutation argument. + pub(crate) permutation_columns: Vec>, + /// Maximum columns per permutation product chunk. + pub(crate) permutation_chunk_len: usize, + /// Number of lookup arguments. + pub(crate) num_lookups: usize, + /// Helper chunks per lookup. + pub(crate) lookup_chunks: Vec, + /// Number of trashcan arguments. + pub(crate) num_trashcans: usize, + /// Number of permutation product commitments. + pub(crate) num_permutation_zs: usize, + /// Number of quotient limb commitments. + pub(crate) num_quotients: usize, + /// Advice query tuples `(column, rotation)`. + pub(crate) advice_queries: Vec<(usize, i32)>, + /// Instance query tuples `(column, rotation)`. + pub(crate) instance_queries: Vec<(usize, i32)>, + pub(crate) num_simple_selectors: usize, + /// Set of fixed-column indices that are *simple selectors* (i.e. + /// multiplicative selectors that the prover sets to 0 or 1 only). + /// These columns have no eval slot in the proof transcript: the + /// verifier substitutes `F::ONE` at their fixed_query index after + /// reading the rest. Step 6 (Yul rewrite) keeps the same convention; + /// the codegen evaluator emits `0x1` directly for these queries. + pub(crate) simple_selector_cols: BTreeSet, + pub(crate) num_committed_instances: usize, + /// Number of Fr scalars in the main eval block of the proof + /// (committed-instance + advice + fixed + perm + lookup + trash + /// evals). When `fewer-point-sets` is on, the codegen bumps this + /// to include the dummy-query evals via `set_num_dummy_evals` so + /// that the memory layout (REVERSED_EVALS_MPTR buffer + + /// comms_mptr_base) and the transcript-loop count both grow by + /// the dummy count. + pub(crate) num_evals: usize, + /// Number of dummy `(commitment, point)` queries the + /// `fewer-point-sets` path appends to the raw query list. Each + /// dummy is read as one extra Fr at the end of the main eval + /// block. Populated lazily by `SolidityGenerator::generate_verifier` + /// after running the PCS dummy-query planner. + pub(crate) num_dummy_evals: usize, + /// Number of distinct point sets returned by the codegen-side + /// `construct_intermediate_sets` simulation. Populated lazily by + /// `SolidityGenerator::generate_verifier` once it has constructed the + /// `Data` and run `kzg::queries`. + pub(crate) num_point_sets: usize, + /// Advice commitment counts by user phase. + pub(crate) num_user_advices: Vec, + /// Challenge counts by user phase. + pub(crate) num_user_challenges: Vec, + /// Advice remapping into phase-packed verifier order. + pub(crate) advice_indices: Vec, + /// Challenge remapping into phase-packed memory order. + pub(crate) challenge_indices: Vec, + /// Last negative rotation used for row-boundary constraints. + pub(crate) rotation_last: i32, +} + +impl ConstraintSystemMeta { + /// Derive metadata from a midnight-proofs `ConstraintSystem`. + /// + /// `nb_committed_instances` is the number of instance columns that the + /// verifier *reads* from the proof transcript (vs. computes locally + /// via Lagrange interpolation). For the poseidon example this is 0; + /// for IVC-style fixtures with committed inputs it would be > 0. + pub(crate) fn new(cs: &ConstraintSystem, nb_committed_instances: usize) -> Self { + let protocol = ProtocolPlan::from_constraint_system(cs, nb_committed_instances); + + Self { + protocol: protocol.clone(), + num_fixeds: protocol.num_fixeds, + permutation_columns: protocol.permutation_columns.clone(), + permutation_chunk_len: protocol.permutation_chunk_len, + num_lookups: protocol.num_lookups, + lookup_chunks: protocol.lookup_chunks.clone(), + num_trashcans: protocol.num_trashcans, + num_permutation_zs: protocol.num_permutation_zs, + num_quotients: protocol.num_quotients, + advice_queries: protocol.advice_queries.iter().map(|q| q.tuple()).collect(), + instance_queries: protocol.instance_queries.iter().map(|q| q.tuple()).collect(), + num_simple_selectors: protocol.num_simple_selectors, + simple_selector_cols: protocol.simple_selector_cols.clone(), + num_committed_instances: protocol.num_committed_instances, + num_evals: protocol.num_main_evals(), + num_dummy_evals: 0, + num_point_sets: 0, + num_user_advices: protocol.num_user_advices.clone(), + num_user_challenges: protocol.num_user_challenges.clone(), + advice_indices: protocol.advice_indices.clone(), + challenge_indices: protocol.challenge_indices.clone(), + rotation_last: protocol.rotation_last, + } + } + + /// Check legacy scalar fields against the typed protocol plan. + pub(crate) fn validate_against_protocol(&self) -> Result<(), String> { + self.protocol.validate()?; + let planned_g1s = self.protocol.num_commitments(); + let scheduled_g1s = self.num_user_advices.iter().sum::() + + self.num_lookups + + self.num_permutation_zs + + self.lookup_chunks.iter().sum::() + + self.num_lookups + + self.num_trashcans + + self.num_quotients; + if planned_g1s != scheduled_g1s { + return Err(format!( + "proof G1 read schedule mismatch: protocol={planned_g1s} metadata={scheduled_g1s}" + )); + } + if self.protocol.num_main_evals() != self.num_main_evals() { + return Err(format!( + "proof eval read schedule mismatch: protocol={} metadata={}", + self.protocol.num_main_evals(), + self.num_main_evals() + )); + } + Ok(()) + } + + /// Setter used by `SolidityGenerator` after running the codegen-side + /// `construct_intermediate_sets` simulation in `kzg::queries`. + pub(crate) fn set_num_point_sets(&mut self, n: usize) { + self.num_point_sets = n; + } + + /// Setter used by `SolidityGenerator` after running + /// the PCS dummy-query planner over the raw query list. + /// Bumps `num_evals` by `n` so that the memory layout of `Data` + /// (REVERSED_EVALS_MPTR buffer + downstream `comms_mptr_base`) and + /// the transcript-loop iteration count in the rendered template + /// both grow to include the dummy slots. + pub(crate) fn set_num_dummy_evals(&mut self, n: usize) { + self.num_dummy_evals = n; + self.num_evals += n; + } + + /// Number of "main" eval Words (the slots used by the gate + /// evaluator, permutation Z, lookup, etc.); excludes the dummy + /// slots appended for fewer-point-sets. + pub(crate) fn num_main_evals(&self) -> usize { + self.num_evals - self.num_dummy_evals + } +} + +// ---------------------------------------------------------------------------- +// Memory-layout helpers (Data, Ptr, EcPoint, Word, ...). The BLS12-381 layout +// uses four words per G1 and `Column` keys for permutation commitments. +// ---------------------------------------------------------------------------- + +#[derive(Debug, Clone)] +pub(crate) struct Data { + /// Memory pointer to user challenge slots. + pub(crate) challenge_mptr: Ptr, + /// Memory pointer to the fixed theta-rooted state window. + pub(crate) theta_mptr: Ptr, + + /// Fixed commitments in VK memory. + pub(crate) fixed_comms: Vec, + /// Permutation commitments keyed by `Column`. + pub(crate) permutation_comms: HashMap, EcPoint>, + /// Advice commitments in phase-packed memory order. + pub(crate) advice_comms: Vec, + /// Per committed-instance column: the EcPoint of the committed + /// instance commitment. Currently always points to `G1_IDENTITY_MPTR` + /// because the zk_stdlib `verify` path passes + /// `committed_pi = G1Affine::identity()`. + pub(crate) committed_instance_comms: Vec, + pub(crate) permutation_z_comms: Vec, + pub(crate) lookup_m_comms: Vec, + pub(crate) lookup_helper_comms: Vec>, + pub(crate) lookup_z_comms: Vec, + pub(crate) trashcan_comms: Vec, + + /// User challenge words. + pub(crate) challenges: Vec, + + /// Locally-computed non-committed instance evaluation. + pub(crate) instance_eval: Word, + /// Per-(committed-instance-column, rotation): the calldata word for + /// that committed instance evaluation. Empty when + /// `num_committed_instances == 0`. + pub(crate) committed_instance_evals: HashMap<(usize, i32), Word>, + pub(crate) advice_evals: HashMap<(usize, i32), Word>, + pub(crate) fixed_evals: HashMap<(usize, i32), Word>, + pub(crate) permutation_evals: HashMap, Word>, + /// Per permutation set: `(z_cur, z_next, z_last)`. `z_last` is + /// `None` for the last set (it has no `last_eval` in the proof; see + /// `midnight_proofs::plonk::permutation::verifier::Committed::evaluate`). + pub(crate) permutation_z_evals: Vec<(Word, Word, Option)>, + /// Per lookup: `(m, [helpers], z, z_next)` evaluations. + pub(crate) lookup_evals: Vec<(Word, Vec, Word, Word)>, + pub(crate) trashcan_evals: Vec, + + pub(crate) computed_quotient_comm: EcPoint, + /// Historical name mirroring `QUOTIENT_EVAL_MPTR`. This is the + /// expected opening scalar for the linearized commitment (`-nu_y(x)`), + /// not an alleged quotient-polynomial evaluation `h(x)`. + pub(crate) computed_quotient_eval: Word, + + /// Word offset (in the verifier's static memory map) of the start of + /// the per-category EIP-2537-padded commitment region. See the + /// `KNOWN BUG` block in `Data::new` for the layout convention. + pub(crate) comms_mptr_base: Ptr, + /// Memory base of the decoded-evals buffer (Optimisation H3). The + /// transcript-side `evaluations` loop spills the decoded scalar value to + /// `REVERSED_EVALS_MPTR + i * 0x20` so that every later eval reference can + /// `mload` instead of rereading calldata. + pub(crate) reversed_evals_mptr: Ptr, + /// Eval Words for the dummy queries appended by the + /// fewer-point-sets path. Empty when the feature is disabled. The + /// dummy buffer is laid out immediately after the main reversed- + /// evals buffer; `dummy_eval_words[i]` points at + /// `REVERSED_EVALS_MPTR + (num_evals + i) * 0x20`. The transcript + /// loop reads `num_dummy_evals` extra Fr scalars after the main + /// eval block and spills them into this buffer the same way the + /// main loop does. + pub(crate) dummy_eval_words: Vec, +} + +impl Data { + /// Bind protocol metadata to concrete proof calldata and verifier memory. + /// + /// The resulting value is the lookup table every emitter uses: it maps + /// proof commitments, evaluations, challenges, and computed helper values + /// to typed `Word`/`EcPoint` handles that render as Yul loads. + pub(crate) fn new( + meta: &ConstraintSystemMeta, + vk: &Halo2VerifyingKey, + memory: &VerifierMemoryLayout, + ) -> Self { + // BLS12-381 G1 commitments occupy 4 words (EIP-2537 padded), so the + // stride between consecutive points is 4 instead of the BN254-era 2. + let fixed_comm_mptr = memory.vk_mptr + vk.constants.len(); + let permutation_comm_mptr = fixed_comm_mptr + G1_WORDS * vk.fixed_comms.len(); + let challenge_mptr = memory.challenge_mptr; + let theta_mptr = memory.theta_mptr; + + // ------------------------------------------------------------ + // Step 8 layout (2026-04-26): + // + // The Solidity proof stream carries BLS12-381 G1 commitments as + // EIP-2537 padded 128-byte points. The PCS / quotient-fold emitters + // consume points as 4 contiguous words from each `EcPoint` base, so + // the template validates and copies each padded G1 into its fixed + // memory MPTR during proof reading. + // + // The seven per-category memory bases below match the constants + // emitted in `templates/contracts/Halo2Verifier.sol`. Each base anchors a + // contiguous run of 4-word slots (one per G1). + // ------------------------------------------------------------ + // -- memory bases for EIP-2537-padded commitments (4 words each) -- + // The template emits a named Solidity constant for each base so + // the hand-written proof-reading loops can use friendly + // identifiers like `ADVICE_COMMS_MPTR_BASE`. The PCS code + // generated below operates on absolute integer offsets so it + // matches the constant values exactly. + // ------------------------------------------------------------ + // Optimisation H3: pre-decode polynomial evaluations. + // + // Each polynomial evaluation in the proof is referenced multiple + // times across the gate evaluator (cp12) and the PCS q_eval Horner + // accumulators (cp14). The Solidity proof shim rewrites scalar proof + // bytes into canonical BE calldata words, so the transcript-side + // evaluations loop can range-check and spill the decoded value once. + // + // We add a single `mstore` per iter to spill the decoded value into a + // contiguous memory buffer at REVERSED_EVALS_MPTR. Every later eval + // reference then becomes `mload(N)` instead of a calldata read. + // + // To make this transparent to the codegen, we construct + // `eval_cptr` as a *Memory* Ptr pointing at REVERSED_EVALS_MPTR + // instead of a Calldata Ptr. All eval Words built via + // `Word::range(eval_cptr)` (committed_instance_evals, + // advice_evals, fixed_evals, permutation_evals, + // permutation_z_evals, lookup_evals, trashcan_evals) inherit + // the Memory location, so `Word::Display` renders them as + // `mload(...)` automatically. + // + // The calldata-side reading code (transcript loop) keeps a + // separate `eval_cd` byte offset for the per-iter + // `calldataload(proof_cptr)`. + // + let reversed_evals_mptr = memory.reversed_evals_mptr; + let eval_cptr = reversed_evals_mptr; + let comms_mptr_base = memory.comms_mptr_base; + let lookup_m_comm_mem_base = memory.lookup_m_comms_mptr_base; + let perm_z_comm_mem_base = memory.perm_z_comms_mptr_base; + let lookup_helper_comm_mem_base = memory.lookup_helper_comms_mptr_base; + let lookup_z_comm_mem_base = memory.lookup_z_comms_mptr_base; + let trashcan_comm_mem_base = memory.trashcan_comms_mptr_base; + + let fixed_comms = EcPoint::range(fixed_comm_mptr).take(meta.num_fixeds).collect(); + // Committed instance commitments: all point at the same memory + // slot (G1_IDENTITY_MPTR) since the zk_stdlib `verify` path + // passes `committed_pi = G1::identity()`. The memory at that + // slot is never written and EVM memory is zero-initialised, so + // the four `mload` reads produce the identity encoding. + let committed_instance_comms: Vec = (0..meta.num_committed_instances) + .map(|_| EcPoint::new(Ptr::memory("G1_IDENTITY_MPTR"))) + .collect(); + let permutation_comms = izip!( + meta.permutation_columns.iter().cloned(), + EcPoint::range(permutation_comm_mptr) + ) + .collect(); + let advice_comms = meta + .advice_indices + .iter() + .map(|idx| comms_mptr_base + G1_WORDS * idx) + .map_into() + .collect(); + let lookup_m_comms = + EcPoint::range(lookup_m_comm_mem_base).take(meta.num_lookups).collect(); + let permutation_z_comms = + EcPoint::range(perm_z_comm_mem_base).take(meta.num_permutation_zs).collect(); + + // Group helpers per lookup by lookup_chunks[i]. + let mut lookup_helper_comms: Vec> = Vec::with_capacity(meta.num_lookups); + let mut helper_cursor = lookup_helper_comm_mem_base; + for &chunks in &meta.lookup_chunks { + let row: Vec = EcPoint::range(helper_cursor).take(chunks).collect(); + helper_cursor = helper_cursor + G1_WORDS * chunks; + lookup_helper_comms.push(row); + } + let lookup_z_comms = + EcPoint::range(lookup_z_comm_mem_base).take(meta.num_lookups).collect(); + let trashcan_comms = + EcPoint::range(trashcan_comm_mem_base).take(meta.num_trashcans).collect(); + let computed_quotient_comm = EcPoint::new(Ptr::memory("QUOTIENT_MPTR")); + + let challenges = meta + .challenge_indices + .iter() + .map(|idx| challenge_mptr + *idx) + .map_into() + .collect_vec(); + let instance_eval = Ptr::memory("INSTANCE_EVAL_MPTR").into(); + + // Main proof evaluations are assigned by the typed protocol plan. + // This replaces the old category-by-category cursor walk with a + // single checked schedule shared with the PCS query construction. + let mut committed_instance_evals: HashMap<(usize, i32), Word> = HashMap::new(); + let mut advice_evals: HashMap<(usize, i32), Word> = HashMap::new(); + let mut fixed_evals: HashMap<(usize, i32), Word> = HashMap::new(); + let mut permutation_evals: HashMap, Word> = HashMap::new(); + let mut permutation_z_slots: Vec<(Option, Option, Option)> = + vec![(None, None, None); meta.num_permutation_zs]; + let mut lookup_slots: Vec = meta + .lookup_chunks + .iter() + .map(|&chunks| (None, vec![None; chunks], None, None)) + .collect(); + let mut trashcan_slots: Vec> = vec![None; meta.num_trashcans]; + + assert_eq!( + meta.protocol.proof.evals.len(), + meta.num_main_evals(), + "protocol proof eval plan must match main eval count" + ); + for (read, word) in meta.protocol.proof.evals.iter().copied().zip(Word::range(eval_cptr)) { + match read { + EvalRead::CommittedInstance(q) => { + committed_instance_evals.insert(q.tuple(), word); + } + EvalRead::Advice(q) => { + advice_evals.insert(q.tuple(), word); + } + EvalRead::Fixed(q) => { + fixed_evals.insert(q.tuple(), word); + } + EvalRead::PermutationCommon { column } => { + permutation_evals.insert(column, word); + } + EvalRead::PermutationZ { set, kind } => { + let slot = + permutation_z_slots.get_mut(set).expect("permutation z eval set in bounds"); + match kind { + PermutationZEval::Cur => slot.0 = Some(word), + PermutationZEval::Next => slot.1 = Some(word), + PermutationZEval::Last => slot.2 = Some(word), + } + } + EvalRead::LookupMultiplicity { lookup } => { + lookup_slots.get_mut(lookup).expect("lookup multiplicity in bounds").0 = + Some(word); + } + EvalRead::LookupHelper { lookup, chunk } => { + lookup_slots + .get_mut(lookup) + .and_then(|(_, helpers, _, _)| helpers.get_mut(chunk)) + .expect("lookup helper eval in bounds") + .replace(word); + } + EvalRead::LookupAccumulator { lookup, rotation } => { + let slot = lookup_slots.get_mut(lookup).expect("lookup accumulator in bounds"); + match rotation { + 0 => slot.2 = Some(word), + 1 => slot.3 = Some(word), + _ => panic!("unsupported lookup accumulator rotation {rotation}"), + } + } + EvalRead::Trash { index } => { + trashcan_slots.get_mut(index).expect("trash eval in bounds").replace(word); + } + } + } + + let permutation_z_evals: Vec<(Word, Word, Option)> = permutation_z_slots + .into_iter() + .enumerate() + .map(|(set_idx, (cur, next, last))| { + let cur = cur.unwrap_or_else(|| panic!("missing permutation z[{set_idx}] cur")); + let next = next.unwrap_or_else(|| panic!("missing permutation z[{set_idx}] next")); + (cur, next, last) + }) + .collect(); + let lookup_evals: Vec<(Word, Vec, Word, Word)> = lookup_slots + .into_iter() + .enumerate() + .map(|(lookup_idx, (m, helpers, z, z_next))| { + let m = m.unwrap_or_else(|| panic!("missing lookup[{lookup_idx}] multiplicity")); + let helpers = helpers + .into_iter() + .enumerate() + .map(|(chunk, helper)| { + helper.unwrap_or_else(|| { + panic!("missing lookup[{lookup_idx}] helper[{chunk}]") + }) + }) + .collect(); + let z = z.unwrap_or_else(|| panic!("missing lookup[{lookup_idx}] accumulator")); + let z_next = z_next + .unwrap_or_else(|| panic!("missing lookup[{lookup_idx}] next accumulator")); + (m, helpers, z, z_next) + }) + .collect(); + let trashcan_evals: Vec = trashcan_slots + .into_iter() + .enumerate() + .map(|(index, word)| word.unwrap_or_else(|| panic!("missing trash eval[{index}]"))) + .collect(); + + let computed_quotient_eval = Ptr::memory("QUOTIENT_EVAL_MPTR").into(); + + Self { + challenge_mptr, + theta_mptr, + fixed_comms, + permutation_comms, + advice_comms, + committed_instance_comms, + permutation_z_comms, + lookup_m_comms, + lookup_helper_comms, + lookup_z_comms, + trashcan_comms, + computed_quotient_comm, + challenges, + instance_eval, + committed_instance_evals, + advice_evals, + fixed_evals, + permutation_evals, + permutation_z_evals, + lookup_evals, + trashcan_evals, + computed_quotient_eval, + comms_mptr_base, + reversed_evals_mptr, + // Default: no dummy queries (fewer-point-sets disabled). + // The caller can populate this via `set_dummy_eval_words` + // after running the PCS dummy-query planner to + // size the buffer. + dummy_eval_words: Vec::new(), + } + } + + /// Allocate `n` dummy eval Words at + /// `REVERSED_EVALS_MPTR + (num_main_evals + i) * 0x20` for i in 0..n, + /// for the `fewer-point-sets` path. + /// + /// Wiring: `SolidityGenerator::generate_verifier` builds `Data` + /// twice. The first build (with `num_dummy_evals = 0`) lets us + /// run the PCS dummy-query planner over the raw query + /// list. The second build is against a `ConstraintSystemMeta` + /// whose `num_evals` already includes the dummies (see + /// [`ConstraintSystemMeta::set_num_dummy_evals`]); this method + /// then populates `dummy_eval_words` with Word handles pointing + /// at the dummy slots in the same `REVERSED_EVALS_MPTR` buffer. + pub(crate) fn set_dummy_eval_words(&mut self, num_main_evals: usize, n: usize) { + self.dummy_eval_words = (0..n) + .map(|i| Word::from(self.reversed_evals_mptr + num_main_evals + i)) + .collect(); + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Location { + /// A value loaded from calldata. + Calldata, + /// A value loaded from EVM memory. + Memory, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Value { + /// Byte offset stored as signed so that the BLS code-gen can compute + /// `ptr - N` even when `N` exceeds the original offset (the result + /// only ever appears as `ptr_end` in `lt(ptr_end, ptr)` style loops + /// where any value strictly less than the smallest visited address is + /// acceptable). + Integer(isize), + /// A symbolic Yul identifier `name`, with an optional byte-offset that + /// will be rendered as `add(name, 0xNN)` (or just `name` when zero). + Identifier(&'static str, isize), +} + +impl Value { + /// Return the concrete offset, panicking for symbolic identifiers. + pub(crate) fn as_usize(&self) -> usize { + match self { + Value::Integer(int) => *int as usize, + Value::Identifier(..) => unreachable!(), + } + } +} + +impl Default for Value { + /// Default to the zero byte offset. + fn default() -> Self { + Self::Integer(0) + } +} + +impl From<&'static str> for Value { + /// Convert a Yul identifier into a symbolic pointer value. + fn from(ident: &'static str) -> Self { + Value::Identifier(ident, 0) + } +} + +impl From for Value { + /// Convert a concrete byte offset into a pointer value. + fn from(int: usize) -> Self { + Value::Integer(int as isize) + } +} + +/// Format an integer byte offset as a stable even-width Yul hex literal. +fn fmt_hex(off: isize) -> String { + let hex = format!("{:x}", off as usize); + if hex.len() % 2 == 1 { + format!("0x0{hex}") + } else { + format!("0x{hex}") + } +} + +impl Display for Value { + /// Render the value as a Yul pointer expression. + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Value::Integer(int) if *int >= 0 => write!(f, "{}", fmt_hex(*int)), + Value::Integer(int) => write!(f, "sub(0, {})", fmt_hex(-*int)), + Value::Identifier(ident, 0) => write!(f, "{ident}"), + Value::Identifier(ident, off) if *off > 0 => { + write!(f, "add({ident}, {})", fmt_hex(*off)) + } + Value::Identifier(ident, off) => { + write!(f, "sub({ident}, {})", fmt_hex(-*off)) + } + } + } +} + +impl Add for Value { + /// Pointer-expression value after word-wise addition. + type Output = Value; + /// Advance by `rhs` EVM words. + fn add(self, rhs: usize) -> Self::Output { + match self { + Value::Integer(int) => Value::Integer(int + (rhs as isize) * WORD_BYTES as isize), + Value::Identifier(name, off) => { + Value::Identifier(name, off + (rhs as isize) * WORD_BYTES as isize) + } + } + } +} + +impl Sub for Value { + /// Pointer-expression value after word-wise subtraction. + type Output = Value; + /// Move backward by `rhs` EVM words. + fn sub(self, rhs: usize) -> Self::Output { + match self { + Value::Integer(int) => Value::Integer(int - (rhs as isize) * WORD_BYTES as isize), + Value::Identifier(name, off) => { + Value::Identifier(name, off - (rhs as isize) * WORD_BYTES as isize) + } + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct Ptr { + loc: Location, + value: Value, +} + +impl Ptr { + /// Construct a pointer at a location with a concrete or symbolic value. + pub(crate) fn new(loc: Location, value: impl Into) -> Self { + Self { + loc, + value: value.into(), + } + } + + /// Construct a memory pointer. + pub(crate) fn memory(value: impl Into) -> Self { + Self::new(Location::Memory, value.into()) + } + + /// Construct a calldata pointer. + pub(crate) fn calldata(value: impl Into) -> Self { + Self::new(Location::Calldata, value.into()) + } + + /// Return whether this pointer targets calldata or memory. + pub(crate) fn loc(&self) -> Location { + self.loc + } + + /// Return the underlying byte-offset expression. + pub(crate) fn value(&self) -> Value { + self.value + } +} + +impl Display for Ptr { + /// Render the pointer's byte-offset expression. + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.value) + } +} + +impl Add for Ptr { + /// Pointer with the same location and advanced value. + type Output = Ptr; + /// Advance by `rhs` EVM words while preserving location. + fn add(mut self, rhs: usize) -> Self::Output { + self.value = self.value + rhs; + self + } +} + +impl Sub for Ptr { + /// Pointer with the same location and rewound value. + type Output = Ptr; + /// Move backward by `rhs` EVM words while preserving location. + fn sub(mut self, rhs: usize) -> Self::Output { + self.value = self.value - rhs; + self + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct Word(Ptr); + +impl Word { + /// Infinite iterator of consecutive word handles starting at `word`. + pub(crate) fn range(word: impl Into) -> impl Iterator { + let ptr = word.into().ptr(); + (0..).map(move |idx| ptr + idx).map_into() + } + + /// Return the pointer backing this word. + pub(crate) fn ptr(&self) -> Ptr { + self.0 + } + + /// Return whether the word is loaded from calldata or memory. + pub(crate) fn loc(&self) -> Location { + self.0.loc() + } +} + +impl Display for Word { + /// Render as the appropriate Yul load expression. + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + // For Calldata-located Words (proof evals/q_evals), the Solidity + // proof shim stores scalars as canonical BE words, so calldataload + // gives the field-element value directly. + match self.0.loc { + Location::Calldata => write!(f, "calldataload({})", self.0.value), + Location::Memory => write!(f, "mload({})", self.0.value), + } + } +} + +impl From for Word { + /// Treat a pointer as the start of one word. + fn from(ptr: Ptr) -> Self { + Self(ptr) + } +} + +/// EIP-2537-padded G1 point handle in calldata or memory. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct EcPoint { + base: Ptr, +} + +impl EcPoint { + /// Construct a point handle at its first word. + pub(crate) fn new(base: impl Into) -> Self { + Self { base: base.into() } + } + + /// Infinite iterator of consecutive padded G1 points. + pub(crate) fn range(base: impl Into) -> impl Iterator { + let base = base.into().base; + (0..).map(move |idx| EcPoint::new(base + 4 * idx)) + } + + /// Return the pointer to the first word. + pub(crate) fn ptr(&self) -> Ptr { + self.base + } +} + +impl From for EcPoint { + /// Treat a pointer as the first word of a padded G1 point. + fn from(ptr: Ptr) -> Self { + Self::new(ptr) + } +} + +// ---------------------------------------------------------------------------- +// BLS12-381 EIP-2537 encoding helpers. The shape of the encoded U256 array +// matches the padded precompile calldata expected by the generated verifier. +// ---------------------------------------------------------------------------- + +/// Split a 48-byte big-endian Fp coordinate into EIP-2537 hi/lo words. +fn fp48_be_to_hi_lo(be: &[u8]) -> (U256, U256) { + debug_assert_eq!(be.len(), BLS_FP_BYTES); + let mut hi_bytes = [0u8; 32]; + hi_bytes[EIP2537_FP_PAD_BYTES..].copy_from_slice(&be[..EIP2537_FP_PAD_BYTES]); + let mut lo_bytes = [0u8; 32]; + lo_bytes.copy_from_slice(&be[EIP2537_FP_PAD_BYTES..]); + (U256::from_be_bytes(hi_bytes), U256::from_be_bytes(lo_bytes)) +} + +/// Encode a midnight-curves G1 point in EIP-2537 padded form (4 u256 words). +/// +/// `Fp::to_repr()` returns *little-endian* 48 bytes (FpRepr). We reverse to +/// big-endian and split (hi=top 16 bytes padded into u256, lo=bottom 32 +/// bytes). +pub(crate) fn g1_to_u256s(ec_point: impl Borrow) -> [U256; 4] { + let Some(coords) = Option::>::from(ec_point.borrow().coordinates()) + else { + return [U256::ZERO; 4]; + }; + let mut x_be = [0u8; BLS_FP_BYTES]; + x_be.copy_from_slice(coords.x().to_repr().as_ref()); + x_be.reverse(); + let mut y_be = [0u8; BLS_FP_BYTES]; + y_be.copy_from_slice(coords.y().to_repr().as_ref()); + y_be.reverse(); + let (x_hi, x_lo) = fp48_be_to_hi_lo(&x_be); + let (y_hi, y_lo) = fp48_be_to_hi_lo(&y_be); + [x_hi, x_lo, y_hi, y_lo] +} + +/// Encode a midnight-curves G2 point in EIP-2537 padded form (8 u256 words). +/// +/// G2 coordinates are `Fp2` with c0/c1 components, each a 48-byte +/// little-endian Fp. EIP-2537 expects (c0, c1) for both x and y, packed +/// in big-endian per coord. The midnight-curves convention matches: +/// each `Fp` coordinate read via `to_repr()` returns LE bytes. +pub(crate) fn g2_to_u256s(ec_point: impl Borrow) -> [U256; 8] { + let Some(coords) = Option::>::from(ec_point.borrow().coordinates()) + else { + return [U256::ZERO; 8]; + }; + + let pack_fp = |fp: midnight_curves::Fp| -> [u8; BLS_FP_BYTES] { + let mut be = [0u8; BLS_FP_BYTES]; + be.copy_from_slice(fp.to_repr().as_ref()); + be.reverse(); + be + }; + + let x = coords.x(); + let y = coords.y(); + + let x0 = pack_fp(x.c0()); + let x1 = pack_fp(x.c1()); + let y0 = pack_fp(y.c0()); + let y1 = pack_fp(y.c1()); + + let (x0_hi, x0_lo) = fp48_be_to_hi_lo(&x0); + let (x1_hi, x1_lo) = fp48_be_to_hi_lo(&x1); + let (y0_hi, y0_lo) = fp48_be_to_hi_lo(&y0); + let (y1_hi, y1_lo) = fp48_be_to_hi_lo(&y1); + [x0_hi, x0_lo, x1_hi, x1_lo, y0_hi, y0_lo, y1_hi, y1_lo] +} + +/// Convert a 32-byte little-endian prime-field representation into `U256`. +pub(crate) fn fe_to_u256(fe: impl Borrow) -> U256 +where + F: PrimeField, + F::Repr: AsRef<[u8]>, +{ + let repr = fe.borrow().to_repr(); + let bytes = repr.as_ref(); + debug_assert_eq!(bytes.len(), 32, "fe_to_u256 expects 32-byte repr"); + let mut le = [0u8; 32]; + le.copy_from_slice(bytes); + U256::from_le_bytes(le) +} + +/// Convert an integer-like value into one 32-byte big-endian EVM word. +pub(crate) fn to_u256_be_bytes(value: T) -> [u8; 32] +where + U256: UintTryFrom, +{ + U256::from(value).to_be_bytes() +} + +#[cfg(test)] +mod tests { + use group::{Curve, Group}; + use midnight_curves::{G1Projective, G2Projective}; + + use super::*; + + #[test] + fn eip2537_encoders_accept_identity_points() { + let g1 = G1Projective::identity().to_affine(); + let g2 = G2Projective::identity().to_affine(); + + assert_eq!(g1_to_u256s(g1), [U256::ZERO; 4]); + assert_eq!(g2_to_u256s(g2), [U256::ZERO; 8]); + } +} diff --git a/proofs/solidity-verifier/src/lowering/kzg/mod.rs b/proofs/solidity-verifier/src/lowering/kzg/mod.rs new file mode 100644 index 000000000..0bd66decf --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/kzg/mod.rs @@ -0,0 +1,2084 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Multi-prepare KZG emitter (Step 5 of docs/architecture/MIGRATION.md). +//! +//! This module mirrors `midfall/proofs/src/poly/kzg/mod.rs::multi_prepare` +//! line-by-line in Yul. The high-level structure is: +//! +//! 1. Build the verifier query list (`queries`). +//! 2. Run a code-gen-time `construct_intermediate_sets` simulation that +//! assigns each commitment to a point set (sorted by ascending cardinality +//! with original-order tiebreak). +//! 3. Emit Yul that: +//! - Pre-computes rotation points `x * omega^rot` for every distinct +//! rotation appearing in any query. +//! - Pre-computes `x1` powers up to `max_set_size - 1`. +//! - Computes `q_com[s]` and `q_eval_set[s]` per set. +//! - Computes `f_eval` via Horner over reverse(point_sets) using Lagrange +//! interpolation at `x3`. +//! - Builds the final commitment via `msm_inner_product` with `x4` powers, +//! plus `f_com` at the highest power. +//! - Scales `z*pi - vG` as the final pairing-side correction and emits the +//! pairing inputs `(pi, final_com - v*G + x3*pi)`. +//! +//! Notes: +//! +//! * The query list includes the linearized commitment built from the +//! quotient commitment(s) and any simple-selector commitments. Its expected +//! opening scalar is not `h(x)`; the verifier reconstructs the batched +//! identity numerator `nu_y(x)` and stores `-nu_y(x)`. The commitment side +//! already carries the `(1 - x^n)` quotient factor. +//! +//! * For point-set inversion we emit one `modexp` precompile call per set +//! (`x3 - p_j`), then locally compose. This is gas-suboptimal but correct +//! and easy to validate; a Montgomery batch invert can replace it once the +//! rest of the verifier is byte-stable. +//! +//! Upstream comments ported here are preserved in +//! `docs/reference/MIDFALL_PROOFS_COMMENT_CORPUS.md` and adapted at the +//! matching emission points below. The most important ones are the Halo 2 +//! multi-opening description, dummy-query/fewer-point-sets behavior, the +//! deterministic ascending-cardinality point-set sort, and the final KZG +//! pairing input `(pi, C - vG + z*pi)`. + +use std::collections::BTreeMap; + +use crate::lowering::{ + abi::proof::ProofCalldataLayout, + encoding::{ConstraintSystemMeta, Data, EcPoint, Location, Ptr, Word}, + layout::{ + self, + memory::{ + FinalMsmShape, PcsMemoryRequirements, VerifierMemoryLayout, G1ADD_INPUT_BYTES, + G1_BYTES, G1_MSM_PAIR_BYTES, PCS_STATIC_WORKING_WORDS, WORD_BYTES, + }, + trace, + }, + protocol::{PcsQuerySource, PermutationZEval}, +}; + +// --------------------------------------------------------------------------- +// Verifier query list. +// --------------------------------------------------------------------------- + +/// A single verifier query: a commitment opened at `omega^rotation * x` +/// to claim `eval`. The current emitter assumes commitments are `OnePiece` +/// (single G1 point) except for the linearization query, whose synthetic +/// commitment is expanded into quotient-limb and simple-selector MSM terms +/// when the Yul is emitted. +#[derive(Clone, Debug)] +pub(crate) struct Query { + /// Rotation exponent for the opening point `omega^rotation * x`. + pub rotation: i32, + /// Commitment being opened. + pub comm: EcPoint, + /// Claimed evaluation scalar. + pub eval: Word, +} + +impl Query { + /// Construct one query from a commitment, rotation, and eval word. + fn new(comm: EcPoint, rotation: i32, eval: Word) -> Self { + Self { + rotation, + comm, + eval, + } + } +} + +/// Build the verifier query list for the codegen verifier. +/// +/// Mirrors the iterator chain in +/// `midfall/proofs/src/plonk/verifier.rs::verify_algebraic_constraints`. +/// The upstream verifier collects all queries for the final multi-open proof; +/// simple multiplicative selector queries are skipped because the custom +/// linearization query carries their commitments and selector accumulators. +pub(crate) fn queries(meta: &ConstraintSystemMeta, data: &Data) -> Vec { + meta.protocol + .pcs_queries + .iter() + .copied() + .map(|source| query_from_plan(source, meta, data)) + .collect() +} + +/// Resolve a typed protocol query source to concrete commitment/eval handles. +fn query_from_plan(source: PcsQuerySource, meta: &ConstraintSystemMeta, data: &Data) -> Query { + match source { + PcsQuerySource::Advice(q) => Query::new( + data.advice_comms[q.column], + q.rotation, + *data + .advice_evals + .get(&q.tuple()) + .expect("advice eval present for every advice query"), + ), + PcsQuerySource::CommittedInstance(q) => Query::new( + data.committed_instance_comms[q.column], + q.rotation, + *data + .committed_instance_evals + .get(&q.tuple()) + .expect("committed instance eval present for every committed instance query"), + ), + PcsQuerySource::PermutationZ { set, kind } => { + let (z_cur, z_next, z_last) = data.permutation_z_evals[set]; + let (rotation, eval) = match kind { + PermutationZEval::Cur => (0, z_cur), + PermutationZEval::Next => (1, z_next), + PermutationZEval::Last => ( + meta.rotation_last, + z_last.expect("permutation last eval present for planned query"), + ), + }; + Query::new(data.permutation_z_comms[set], rotation, eval) + } + PcsQuerySource::LookupMultiplicity { lookup } => { + let (m_eval, _, _, _) = &data.lookup_evals[lookup]; + Query::new(data.lookup_m_comms[lookup], 0, *m_eval) + } + PcsQuerySource::LookupHelper { lookup, chunk } => { + let (_, h_evals, _, _) = &data.lookup_evals[lookup]; + Query::new(data.lookup_helper_comms[lookup][chunk], 0, h_evals[chunk]) + } + PcsQuerySource::LookupAccumulator { lookup, rotation } => { + let (_, _, z_eval, z_next_eval) = &data.lookup_evals[lookup]; + let eval = match rotation { + 0 => *z_eval, + 1 => *z_next_eval, + _ => panic!("unsupported lookup accumulator rotation {rotation}"), + }; + Query::new(data.lookup_z_comms[lookup], rotation, eval) + } + PcsQuerySource::Trash { index } => { + Query::new(data.trashcan_comms[index], 0, data.trashcan_evals[index]) + } + PcsQuerySource::Fixed(q) => Query::new( + data.fixed_comms[q.column], + q.rotation, + *data.fixed_evals.get(&q.tuple()).expect("fixed eval present"), + ), + PcsQuerySource::PermutationCommon { column } => Query::new( + data.permutation_comms[&column], + 0, + data.permutation_evals[&column], + ), + PcsQuerySource::Linearization => { + Query::new(data.computed_quotient_comm, 0, data.computed_quotient_eval) + } + } +} + +// --------------------------------------------------------------------------- +// Dummy-query computation (codegen-time port of +// `midfall/proofs/src/poly/kzg/utils.rs::compute_dummy_queries`, gated +// behind the `fewer-point-sets` feature on the Rust side). +// --------------------------------------------------------------------------- + +/// A dummy query: append `(query_index, point)` to the raw query list, +/// reusing `raw_queries[query_index].comm` as the commitment and +/// allocating a new eval Word for the dummy proof scalar. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct DummyQuery { + /// Index into the *raw* query list (the output of `queries`) + /// whose commitment this dummy reuses. + pub query_index: usize, + /// Rotation point at which the dummy query opens. + pub rotation: i32, +} + +/// Run the dummy-query computation over the raw query list. +/// +/// Mirrors `compute_dummy_queries` 1:1: groups queries by commitment +/// (using the [`EcPoint`] identity, which is the underlying memory +/// pointer), unions all non-singleton point sets, and emits the +/// missing `(first_index, point)` pairs that, once added, make every +/// non-singleton point set identical. +/// Upstream comment: add dummy queries to reduce the number of distinct +/// point sets that the KZG verifier must batch. +/// +/// The output order is deterministic (insertion order of groups * +/// insertion order of points), matching the prover's transcript layout. +pub(crate) fn compute_dummy_queries(queries: &[Query]) -> Vec { + // Group by commitment, tracking each group's first occurrence + // index in `queries` and the rotations already covered. + let mut groups: Vec<(usize, Vec)> = Vec::new(); + for (i, q) in queries.iter().enumerate() { + match groups.iter_mut().find(|(idx, _)| queries[*idx].comm == q.comm) { + Some((_, points)) if !points.contains(&q.rotation) => { + points.push(q.rotation); + } + // Same `(commitment, rotation)` already recorded. Eval consistency + // is enforced upstream in `construct_intermediate_sets_impl`, so + // here we silently skip the redundant rotation rather than panic; + // this matters when several queries (e.g. multiple committed- + // instance columns) legally share `G1_IDENTITY_MPTR` at rotation 0. + Some(_) => continue, + None => groups.push((i, vec![q.rotation])), + } + } + + // Union of all non-singleton point sets, in insertion order. + let mut union: Vec = Vec::new(); + for (_, points) in &groups { + if points.len() <= 1 { + continue; + } + for &p in points { + if !union.contains(&p) { + union.push(p); + } + } + } + + // Emit missing (first_index, point) pairs in deterministic order. + let mut out: Vec = Vec::new(); + for (idx, existing) in &groups { + for &p in &union { + if !existing.contains(&p) { + out.push(DummyQuery { + query_index: *idx, + rotation: p, + }); + } + } + } + out +} + +/// Augment `raw_queries` with dummy queries; the i-th dummy uses +/// `dummy_eval_words[i]` as its eval Word. Caller must ensure +/// `dummy_eval_words.len() == compute_dummy_queries(raw_queries).len()`. +pub(crate) fn augment_queries_with_dummies( + raw_queries: &[Query], + dummy_eval_words: &[Word], +) -> Vec { + let dummies = compute_dummy_queries(raw_queries); + assert_eq!( + dummies.len(), + dummy_eval_words.len(), + "augment_queries_with_dummies: expected {} dummy eval Words, got {}", + dummies.len(), + dummy_eval_words.len() + ); + let mut out = raw_queries.to_vec(); + for (d, eval) in dummies.iter().zip(dummy_eval_words.iter()) { + let comm = raw_queries[d.query_index].comm; + out.push(Query::new(comm, d.rotation, *eval)); + } + out +} + +// --------------------------------------------------------------------------- +// IntermediateSets simulation (codegen-time port of +// `midfall/proofs/src/poly/kzg/utils.rs::construct_intermediate_sets`). +// --------------------------------------------------------------------------- + +#[derive(Clone, Debug)] +pub(crate) struct CommitmentEntry { + /// Index into the sorted `point_sets` vector. + pub set_index: usize, + /// G1 commitment (deduplicated). + pub comm: EcPoint, + /// Evals aligned with `point_sets[set_index]`. + pub evals: Vec, +} + +/// Codegen-time result of the KZG intermediate-set construction. +#[derive(Clone, Debug)] +pub(crate) struct IntermediateSets { + /// Unique commitments with eval vectors aligned to their point set. + pub commitments: Vec, + /// Distinct rotation sets, sorted by verifier order. + pub point_sets: Vec>, +} + +/// Run the intermediate-set construction over the codegen-time queries. +/// +/// `EcPoint` comparisons use derived `PartialEq`, which compares the +/// underlying memory pointer; that is exactly the identity semantics we +/// want (same memory location = same commitment). +fn construct_intermediate_sets_impl(queries: &[Query]) -> IntermediateSets { + // Step 1: build commitment_map (one entry per unique commitment) and + // a point_index map (one entry per unique rotation). + let mut commitment_map: Vec<(EcPoint, Vec, Vec)> = Vec::new(); + let mut point_index_of: Vec = Vec::new(); + + for query in queries { + let point_idx = match point_index_of.iter().position(|p| *p == query.rotation) { + Some(i) => i, + None => { + point_index_of.push(query.rotation); + point_index_of.len() - 1 + } + }; + + if let Some(slot) = commitment_map.iter_mut().find(|(c, _, _)| *c == query.comm) { + // Two queries can share `(commitment, rotation)` legally when the + // protocol points multiple columns at the same slot (e.g. every + // committed-instance column shares `G1_IDENTITY_MPTR` in the + // zk_stdlib `verify` path). Collapse them into the existing entry + // when their evals also agree; otherwise the proof claims the + // same polynomial opens to two different values, which is a + // protocol bug and must be rejected. + if let Some(existing_pos) = slot.1.iter().position(|pi| *pi == point_idx) { + assert_eq!( + slot.2[existing_pos], query.eval, + "duplicate (commitment, rotation) query with conflicting evals" + ); + continue; + } + slot.1.push(point_idx); + slot.2.push(query.eval); + } else { + commitment_map.push((query.comm, vec![point_idx], vec![query.eval])); + } + } + + // Step 2: bucket commitments by their point-index set (BTreeSet so + // the bucket key is order-insensitive and deterministically ordered). + let mut point_idx_sets: BTreeMap, usize> = BTreeMap::new(); + for (_, point_indices, _) in &commitment_map { + let mut sorted = point_indices.clone(); + sorted.sort_unstable(); + sorted.dedup(); + let n = point_idx_sets.len(); + point_idx_sets.entry(sorted).or_insert(n); + } + + // Step 3: for each commitment, look up its set_index and align evals + // with the *sorted* point_indices order. + let mut commitments: Vec = Vec::with_capacity(commitment_map.len()); + for (comm, point_indices, evals) in commitment_map { + let mut sorted_indices = point_indices.clone(); + sorted_indices.sort_unstable(); + sorted_indices.dedup(); + + let set_index = *point_idx_sets.get(&sorted_indices).unwrap(); + + // Build evals[k] = eval whose point_index lands at sorted_indices[k]. + let mut aligned_evals = vec![None; sorted_indices.len()]; + for (pi, ev) in point_indices.iter().zip(evals.iter()) { + let pos = sorted_indices.iter().position(|p| p == pi).unwrap(); + aligned_evals[pos] = Some(*ev); + } + commitments.push(CommitmentEntry { + set_index, + comm, + evals: aligned_evals.into_iter().map(Option::unwrap).collect(), + }); + } + + // Step 4: turn point_idx sets into actual rotation lists. The slot + // assigned to each set is its *insertion* order, captured at + // `or_insert(n)` above. The BTreeMap iteration here is by `Vec` + // lex order purely so we visit every entry once; the index that lands + // in `point_sets[..]` is `set_idx` (insertion order), which is also + // the tiebreak key used by `sort_sets`. Anyone replacing this map + // with a `HashMap` must preserve that insertion-order capture, not + // the iteration order itself. + let mut point_sets: Vec> = vec![Vec::new(); point_idx_sets.len()]; + for (idx_set, &set_idx) in point_idx_sets.iter() { + let rotations: Vec = idx_set.iter().map(|i| point_index_of[*i]).collect(); + point_sets[set_idx] = rotations; + } + + IntermediateSets { + commitments, + point_sets, + } +} + +/// Sort point sets by ascending cardinality (tiebreaker by original set +/// index). The Rust KZG verifier relies on this deterministic order so the +/// first point set contains the fixed commitments opened at x, and the +/// in-circuit verifier can collapse the same MSMs. Returns a new +/// IntermediateSets with `set_index` values rewritten to point to the sorted +/// positions. +fn sort_sets(input: IntermediateSets) -> IntermediateSets { + let mut order: Vec = (0..input.point_sets.len()).collect(); + order.sort_by_key(|&i| (input.point_sets[i].len(), i)); + + // remap[old_idx] = new_idx + let mut remap = vec![0usize; input.point_sets.len()]; + for (new_idx, &old_idx) in order.iter().enumerate() { + remap[old_idx] = new_idx; + } + + let point_sets = order.iter().map(|&i| input.point_sets[i].clone()).collect(); + let commitments = input + .commitments + .into_iter() + .map(|mut c| { + c.set_index = remap[c.set_index]; + c + }) + .collect(); + + IntermediateSets { + commitments, + point_sets, + } +} + +/// Build sorted intermediate sets, applying dummy queries when configured. +pub(crate) fn intermediate_sets(meta: &ConstraintSystemMeta, data: &Data) -> IntermediateSets { + let raw = queries(meta, data); + let queries = if data.dummy_eval_words.is_empty() { + raw + } else { + // fewer-point-sets path: append dummy queries built against the + // pre-allocated dummy eval Words (Data::set_dummy_evals). + augment_queries_with_dummies(&raw, &data.dummy_eval_words) + }; + sort_sets(construct_intermediate_sets_impl(&queries)) +} + +/// Return the number of distinct KZG point sets after dummy-query planning. +pub(crate) fn num_point_sets(meta: &ConstraintSystemMeta, data: &Data) -> usize { + intermediate_sets(meta, data).point_sets.len() +} + +/// One proof or verifier-memory G1 handle tracked by generator invariants. +#[derive(Clone, Debug, PartialEq, Eq)] +struct TrackedG1 { + label: String, + point: EcPoint, +} + +impl TrackedG1 { + /// Attach a diagnostic label to one planned G1 memory handle. + fn new(label: impl Into, point: EcPoint) -> Self { + Self { + label: label.into(), + point, + } + } +} + +/// Return the planned memory handles for quotient limb commitments. +fn quotient_limb_comms(meta: &ConstraintSystemMeta, memory: &VerifierMemoryLayout) -> Vec { + EcPoint::range(memory.quotient_limb_comms_mptr_base) + .take(meta.num_quotients) + .collect() +} + +/// List every proof-controlled G1 absorbed into the transcript. +fn proof_absorbed_g1s( + meta: &ConstraintSystemMeta, + data: &Data, + memory: &VerifierMemoryLayout, +) -> Vec { + let mut absorbed = Vec::new(); + + absorbed.extend( + data.advice_comms + .iter() + .copied() + .enumerate() + .map(|(idx, point)| TrackedG1::new(format!("advice[{idx}]"), point)), + ); + absorbed.extend( + data.lookup_m_comms + .iter() + .copied() + .enumerate() + .map(|(idx, point)| TrackedG1::new(format!("lookup_multiplicity[{idx}]"), point)), + ); + absorbed.extend( + data.permutation_z_comms + .iter() + .copied() + .enumerate() + .map(|(idx, point)| TrackedG1::new(format!("permutation_product[{idx}]"), point)), + ); + for (lookup_idx, helpers) in data.lookup_helper_comms.iter().enumerate() { + absorbed.extend( + helpers.iter().copied().enumerate().map(|(chunk_idx, point)| { + TrackedG1::new(format!("lookup_helper[{lookup_idx}][{chunk_idx}]"), point) + }), + ); + } + absorbed.extend( + data.lookup_z_comms + .iter() + .copied() + .enumerate() + .map(|(idx, point)| TrackedG1::new(format!("lookup_accumulator[{idx}]"), point)), + ); + absorbed.extend( + data.trashcan_comms + .iter() + .copied() + .enumerate() + .map(|(idx, point)| TrackedG1::new(format!("trashcan[{idx}]"), point)), + ); + absorbed.extend( + quotient_limb_comms(meta, memory) + .into_iter() + .enumerate() + .map(|(idx, point)| TrackedG1::new(format!("quotient_limb[{idx}]"), point)), + ); + absorbed.push(TrackedG1::new("f_com", EcPoint::new(memory.f_com_mptr))); + absorbed.push(TrackedG1::new("pi", EcPoint::new(memory.pi_mptr))); + + absorbed +} + +/// List every absorbed proof G1 that is later consumed by a validating +/// EIP-2537 precompile path. +fn precompile_validated_g1s( + meta: &ConstraintSystemMeta, + data: &Data, + memory: &VerifierMemoryLayout, +) -> Vec { + let sets = intermediate_sets(meta, data); + let g1_identity = EcPoint::new(Ptr::memory("G1_IDENTITY_MPTR")); + let linearization_comm = data.computed_quotient_comm; + let mut validated = Vec::new(); + + for entry in sets.commitments { + if entry.comm == g1_identity { + continue; + } + + if entry.comm == linearization_comm { + validated.extend( + quotient_limb_comms(meta, memory).into_iter().enumerate().map(|(idx, point)| { + TrackedG1::new(format!("quotient_limb[{idx}] via final PCS G1MSM"), point) + }), + ); + } else { + validated.push(TrackedG1::new("PCS query commitment", entry.comm)); + } + } + + validated.push(TrackedG1::new( + "f_com via final PCS G1MSM", + EcPoint::new(memory.f_com_mptr), + )); + validated.push(TrackedG1::new( + "pi via final pairing input", + EcPoint::new(memory.pi_mptr), + )); + + validated +} + +/// Ensure transcript-absorbed proof points are not trusted before subgroup +/// validation. +fn validate_g1_precompile_coverage( + absorbed: &[TrackedG1], + precompile_inputs: &[TrackedG1], +) -> Result<(), String> { + let missing: Vec<&TrackedG1> = absorbed + .iter() + .filter(|absorbed| { + !precompile_inputs.iter().any(|candidate| candidate.point == absorbed.point) + }) + .collect(); + + if let Some(first) = missing.first() { + return Err(format!( + "{} absorbed proof G1(s) are not consumed by a later EIP-2537 G1MSM/pairing precompile; first missing: {} at {}", + missing.len(), + first.label, + first.point.ptr() + )); + } + + Ok(()) +} + +/// Validate the concrete proof-read to precompile-use boundary. +/// +/// `common_uncompressed_g1` absorbs proof G1 points into Fiat-Shamir before +/// the verifier has run any independent subgroup check. The generator is +/// allowed to do this only if every user-controlled absorbed point is later +/// consumed by an EIP-2537 G1MSM or pairing input, whose implementation +/// rejects invalid BLS12-381 points. The protocol plan covers the logical PCS +/// queries; this invariant checks the concrete memory handles emitted by the +/// generator, including quotient limbs hidden behind the synthetic +/// linearization commitment. +pub(crate) fn validate_absorbed_g1_precompile_coverage( + meta: &ConstraintSystemMeta, + data: &Data, + memory: &VerifierMemoryLayout, + proof_layout: &ProofCalldataLayout, +) -> Result<(), String> { + let absorbed = proof_absorbed_g1s(meta, data, memory); + let expected = proof_layout.total_g1_count(); + if absorbed.len() != expected { + return Err(format!( + "absorbed proof G1 count mismatch: generator tracked {} point(s), proof layout expects {expected}", + absorbed.len() + )); + } + + let precompile_inputs = precompile_validated_g1s(meta, data, memory); + validate_g1_precompile_coverage(&absorbed, &precompile_inputs) +} + +/// Group commitment entries by their sorted point-set index. +fn commitments_by_set(sets: &IntermediateSets, n_sets: usize) -> Vec> { + let mut by_set: Vec> = vec![Vec::new(); n_sets]; + for c in &sets.commitments { + by_set[c.set_index].push(c); + } + by_set +} + +/// Number of MSM terms represented by the synthetic linearization commitment. +fn linearization_term_count(meta: &ConstraintSystemMeta) -> usize { + // `linearization/verifier.rs::compute_linearization_commitment` builds an + // MSM for + // S_0*id_0(x) + y*S_1*id_1(x) + ... - + // (h_0 + x^(n-1)h_1 + ...)*(x^n - 1) + // In the outer single-H layout this quotient slice has exactly one term, + // so the scalar is just `(1 - x^n)`. + // Fully evaluated identities are not included on the commitment side; + // they are accumulated, negated, and used as the expected eval scalar. + meta.num_quotients + meta.simple_selector_cols.len() +} + +/// Number of non-identity q_com MSM terms needed for one point set. +fn q_com_terms_for_set( + meta: &ConstraintSystemMeta, + data: &Data, + commitments_in_set: &[&CommitmentEntry], +) -> usize { + let g1_identity = EcPoint::new(Ptr::memory("G1_IDENTITY_MPTR")); + let linearization_comm = data.computed_quotient_comm; + let linearization_term_count = linearization_term_count(meta); + + commitments_in_set + .iter() + .map(|entry| { + if entry.comm == g1_identity { + 0 + } else if entry.comm == linearization_comm { + linearization_term_count + } else { + 1 + } + }) + .sum() +} + +/// Maximum trace-only q_com MSM size across all point sets. +fn q_com_trace_terms( + meta: &ConstraintSystemMeta, + data: &Data, + by_set: &[Vec<&CommitmentEntry>], +) -> usize { + by_set + .iter() + .map(|commitments| q_com_terms_for_set(meta, data, commitments)) + .max() + .unwrap_or(0) +} + +/// Shape of the fused final PCS MSM after expanding linearization terms. +fn final_msm_shape( + meta: &ConstraintSystemMeta, + data: &Data, + by_set: &[Vec<&CommitmentEntry>], +) -> FinalMsmShape { + let g1_identity = EcPoint::new(Ptr::memory("G1_IDENTITY_MPTR")); + let linearization_comm = data.computed_quotient_comm; + let linearization_term_count = linearization_term_count(meta); + let terms = by_set + .iter() + .flat_map(|commitments| commitments.iter()) + .filter(|entry| entry.comm != g1_identity) + .map(|entry| { + if entry.comm == linearization_comm { + linearization_term_count + } else { + 1 + } + }) + .sum::() + + 1; // f_com + + FinalMsmShape::from_terms(terms) +} + +/// Minimum point-set width where generated q_eval accumulation uses a loop. +pub(super) const Q_EVAL_ROLL_THRESHOLD: usize = 4; + +/// Emission strategy for a point set's q_eval fold. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum QEvalStrategy { + /// Use a loop over memory-backed eval source addresses. + Rolled, + /// Emit straight-line arithmetic. + Unrolled, +} + +/// Pick the q_eval emission strategy for commitments in one point set. +fn q_eval_strategy(commitments: &[&CommitmentEntry]) -> QEvalStrategy { + let Some(first) = commitments.first() else { + return QEvalStrategy::Unrolled; + }; + if commitments.len() < Q_EVAL_ROLL_THRESHOLD { + return QEvalStrategy::Unrolled; + } + + let n_rot = first.evals.len(); + let can_roll = commitments.iter().all(|commitment| { + commitment.evals.len() == n_rot + && commitment.evals.iter().all(|eval| matches!(eval.loc(), Location::Memory)) + }); + + if can_roll { + QEvalStrategy::Rolled + } else { + QEvalStrategy::Unrolled + } +} + +/// Compute all PCS scratch-window and MSM size requirements. +pub(crate) fn memory_requirements( + meta: &ConstraintSystemMeta, + data: &Data, +) -> PcsMemoryRequirements { + let sets = intermediate_sets(meta, data); + let n_sets = sets.point_sets.len(); + if n_sets == 0 { + return PcsMemoryRequirements::default(); + } + + let by_set = commitments_by_set(&sets, n_sets); + let commitments_per_set = by_set.iter().map(Vec::len); + + let mut distinct_rotations: Vec = + sets.point_sets.iter().flat_map(|s| s.iter().copied()).collect(); + distinct_rotations.sort_unstable(); + distinct_rotations.dedup(); + + let q_eval_source_table_words = by_set + .iter() + .zip(sets.point_sets.iter()) + .filter(|(commitments, _)| q_eval_strategy(commitments) == QEvalStrategy::Rolled) + .map(|(commitments, rotations)| commitments.len() * rotations.len()) + .max() + .unwrap_or(0); + let q_com_trace_terms = q_com_trace_terms(meta, data, &by_set); + + PcsMemoryRequirements { + rot_points_words: distinct_rotations.len(), + x1_powers_words: commitments_per_set.max().unwrap_or(0), + // Current q_com materialization is fused into the final MSM scratch + // instead of Q_COM_MPTR. Keep the field explicit so a future + // Q_COM_MPTR user must also participate in layout validation. + q_com_words: 0, + q_eval_set_words: sets.point_sets.iter().map(Vec::len).sum(), + q_eval_source_table_words, + q_com_trace_msm: FinalMsmShape::from_terms(q_com_trace_terms), + final_msm: final_msm_shape(meta, data, &by_set), + } +} + +/// Returns the number of dummy queries (and thus the number of extra +/// Fr scalars in the proof's eval block) emitted by the +/// fewer-point-sets path for this circuit's query topology. Call this +/// over the *raw* (unaugmented) queries to size the dummy buffer +/// before constructing the augmented `Data`. +pub(crate) fn num_dummy_queries(meta: &ConstraintSystemMeta, data: &Data) -> usize { + compute_dummy_queries(&queries(meta, data)).len() +} + +// --------------------------------------------------------------------------- +// Yul emission. +// --------------------------------------------------------------------------- + +pub(crate) fn static_working_memory_size() -> usize { + // Reserve generous scratch for the pairing call and intermediate + // q_com / final_com slots. The pairing precompile needs 2 G1 (8 + // words) + 2 G2 (16 words) + 1 output word = 25 words. We round up + // to 32 to leave room for the in-Yul accumulator slots used during + // MSM construction. + PCS_STATIC_WORKING_WORDS +} + +/// Emit the multi-prepare Yul body. The output is the same +/// vec-of-vec-of-strings shape the rest of the codegen uses; each inner +/// `Vec` is a discrete Yul code block (rendered between `{` and `}` in +/// the template). +#[allow(clippy::vec_init_then_push)] +pub(crate) fn computations( + meta: &ConstraintSystemMeta, + data: &Data, + memory: &VerifierMemoryLayout, + truncated_challenges: bool, + trace: bool, +) -> Vec> { + /// 128-bit mask for `truncate(scalar)` in midnight-proofs: + /// `truncate` keeps the lower `ceil(NUM_BITS/8)/2 = 16` bytes + /// of the LE Fr representation, which is the lower 128 bits. + const TRUNC_MASK_128: &str = "0xffffffffffffffffffffffffffffffff"; + let sets = intermediate_sets(meta, data); + let n_sets = sets.point_sets.len(); + if n_sets == 0 { + return Vec::new(); + } + + // The emitted blocks below adapt the Rust `multi_prepare` flow: + // construct/sort point sets, fold q_eval vectors, interpolate at x3, + // build the final commitment with x4 powers, then prepare the final KZG + // pairing inputs `(pi, final_com - vG + x3*pi)`. + // Per-set commitment list. Reused by the q_eval fold block and the + // fused final commitment MSM. + let by_set = commitments_by_set(&sets, n_sets); + + // The number of x1 powers is bounded by the largest commitments-per-set + // count (one power per commitment within a set). + let nb_x1_powers: usize = by_set.iter().map(Vec::len).max().unwrap_or(0); + + // Distinct rotations encountered, sorted; emit code that stores + // `x * omega^rot` at a known scratch slot per rotation. + let mut distinct_rotations: Vec = + sets.point_sets.iter().flat_map(|s| s.iter().copied()).collect(); + distinct_rotations.sort_unstable(); + distinct_rotations.dedup(); + + let mut blocks: Vec> = Vec::new(); + + // ------------------------------------------------------------------ + // Block 1: pre-compute rotation points (x * omega^rot). + // + // We stash each `x*omega^rot` at scratch offset + // ROT_POINTS_MPTR + 32 * idx_of(rot) + // where `idx_of(rot)` is the index of `rot` in `distinct_rotations`. + // + // The Yul template (Step 6) will define ROT_POINTS_MPTR. For now we + // just emit symbolic references; the macro layout is finalised + // alongside Step 6. + // ------------------------------------------------------------------ + { + let mut lines: Vec = Vec::new(); + lines.push(format!( + "// {} distinct rotation(s)", + distinct_rotations.len() + )); + lines.push("let x := mload(X_MPTR)".to_string()); + lines.push("let omega := mload(OMEGA_MPTR)".to_string()); + lines.push("let omega_inv := mload(OMEGA_INV_MPTR)".to_string()); + + // Walk forward from rotation 0 through max positive rotation, + // multiplying by omega; then walk back to min negative rotation, + // multiplying by omega_inv. + let max_rot = *distinct_rotations.iter().max().unwrap_or(&0); + let min_rot = *distinct_rotations.iter().min().unwrap_or(&0); + + let store_rot = |rot: i32| -> Option { + distinct_rotations.iter().position(|r| *r == rot).map(|idx| { + format!( + "mstore(add(ROT_POINTS_MPTR, {:#x}), x_pow_of_omega)", + idx * WORD_BYTES + ) + }) + }; + + // Rotation 0 = x. + lines.push("let x_pow_of_omega := x".to_string()); + if let Some(s) = store_rot(0) { + lines.push(s); + } + + // Forward walk for positive rotations. + for rot in 1..=max_rot { + lines.push("x_pow_of_omega := mulmod(x_pow_of_omega, omega, r)".to_string()); + if let Some(s) = store_rot(rot) { + lines.push(s); + } + } + + // Backward walk for negative rotations. + if min_rot < 0 { + lines.push("x_pow_of_omega := x".to_string()); + for rot in (min_rot..0).rev() { + lines.push("x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r)".to_string()); + if let Some(s) = store_rot(rot) { + lines.push(s); + } + } + } + + if trace { + // Trace-only serialization of each point set's rotation values. + // We borrow `Q_EVAL_SET_MPTR` as scratch for the `log1` payload + // because Block 3 unconditionally rewrites those words with the + // real `q_eval_set[s][k]` folds before any later block reads + // them. Any future reader of `Q_EVAL_SET_MPTR` between this + // block and Block 3 must move the trace scratch elsewhere. + for (set_idx, points) in sets.point_sets.iter().enumerate() { + lines.push(format!( + "// trace serialized PCS point set {set_idx} ({} point(s))", + points.len() + )); + for (point_idx, rot) in points.iter().enumerate() { + let rot_idx = distinct_rotations + .iter() + .position(|r| r == rot) + .expect("point-set rotation present in distinct rotations"); + lines.push(format!( + "mstore(add(Q_EVAL_SET_MPTR, {:#x}), mload(add(ROT_POINTS_MPTR, {:#x})))", + point_idx * WORD_BYTES, + rot_idx * WORD_BYTES + )); + } + lines.push(format!( + "log1(Q_EVAL_SET_MPTR, {:#x}, {})", + points.len() * WORD_BYTES, + trace::PCS_SERIALIZED_POINT_SET_BASE + set_idx as u64 + )); + } + } + + blocks.push(lines); + } + + // ------------------------------------------------------------------ + // Block 2: pre-compute x1 powers (x1^0 .. x1^(nb_x1_powers - 1)). + // + // Stashed at X1_POWERS_MPTR + 32 * idx (idx = 0..nb_x1_powers - 1). + // ------------------------------------------------------------------ + if nb_x1_powers > 0 { + let mut lines: Vec = Vec::new(); + lines.push(format!("// pre-compute {nb_x1_powers} x1 power(s)")); + lines.push("let x1 := mload(X1_MPTR)".to_string()); + lines.push("mstore(X1_POWERS_MPTR, 1)".to_string()); + if nb_x1_powers > 1 { + // Roll the power-of-x1 sequence into a Yul `for` loop. + // The unrolled emission (32 sequential mulmod+mstore + // pairs for the Poseidon fixture) was costing ~26 kg + // — far above the ~700 gas the arithmetic itself + // requires. solc-via-ir struggles to register-allocate + // 32 unrolled mulmods sharing one accumulator, and the + // unrolled mstore-add chain inflates each line to + // ~50-60 gas of dispatch overhead. The rolled loop + // restores the basic-block heuristic and lets the + // optimizer schedule the inner body once. + // + // Per iteration (rolled): + // lt + add(i+1) + add(p) ≈ 9 gas (loop control) + // mulmod + mstore ≈ 11 gas (body) + // ---- + // ~20 gas/iter, × 32 = ~640 gas + 50 setup = ~700 gas + // + // truncated-challenges: midnight-proofs + // proofs/src/poly/kzg/mod.rs computes + // power[i] = truncate(x1^i) + // where the internal x1^i accumulator stays at full + // precision (powers(x1).map(truncate) in Rust). We mirror + // that here by storing `and(acc, mask)` while keeping + // `acc` itself in full precision. The +3 gas/iter cost is + // negligible vs the ~640 gas of the loop body. + let last = nb_x1_powers - 1; + lines.push("let acc := 1".to_string()); + lines.push("let p := X1_POWERS_MPTR".to_string()); + lines.push(format!( + "for {{ let i := 0 }} lt(i, {last:#x}) {{ i := add(i, 1) }} {{" + )); + lines.push(format!(" p := add(p, {WORD_BYTES:#x})")); + lines.push(" acc := mulmod(acc, x1, r)".to_string()); + if truncated_challenges { + lines.push(format!(" mstore(p, and(acc, {TRUNC_MASK_128}))")); + } else { + lines.push(" mstore(p, acc)".to_string()); + } + lines.push("}".to_string()); + } + blocks.push(lines); + } + + // ------------------------------------------------------------------ + // Block 3: per-set q_eval_set computation. + // + // For each set s: + // q_eval_set[s] = sum_{i, c in commitments of s} x1[i] * c.eval[?] + // + // The commitment ordering inside a set follows the order in which + // commitments appear in `sets.commitments` (which is the order in + // which queries were enumerated; this matches the midnight-proofs + // for_each iteration order). + // + // Storage: + // Q_EVAL_SET_MPTR + 0x20 * s : q_eval_set[s] (1 Fq word) + // ------------------------------------------------------------------ + { + // ------------------------------------------------------------------ + // Memory layout for Block 3 (rolled q_eval). + // + // For "wide" memory-backed sets (m >= Q_EVAL_ROLL_THRESHOLD), the `m * n_rot` + // straight-line addmod block is collapsed into a single Yul `for` + // loop. The loop body indexes a pre-staged scratch table holding + // per-rotation eval addresses for each commitment. + // + // Per-set scratch layout: + // EVAL_SRC_TABLE_MPTR m * n_rot * 0x20 bytes (eval addrs) + // + // The table is placed above every commitment slot. Block 5 reuses + // the same scratch region for the fused final MSM after the q_eval + // folds have been persisted to Q_EVAL_SET_MPTR. + // + // Sets with m < Q_EVAL_ROLL_THRESHOLD, ragged rotation rows, or + // calldata-backed evals keep the unrolled emission. That preserves + // correctness if a future proof layout stops spilling evals to memory. + // ------------------------------------------------------------------ + let eval_src_table_mptr: usize = memory.pcs_q_eval_source_table_mptr; + + for (set_idx, commitments_in_set) in by_set.iter().enumerate() { + let mut lines: Vec = Vec::new(); + let q_eval_base = format!("add(Q_EVAL_SET_MPTR, {:#x})", set_idx * WORD_BYTES); + let m = commitments_in_set.len(); + + // q_eval_set[s] is itself a *vector* of |set| evaluations + // (not a single scalar): one per rotation in the set's + // point list. midnight-proofs computes + // q_polys[s] = sum_i x1^i * poly_i (across commits in s) + // q_eval_set[s] = sum_i x1^i * evals_i_at_set_points + // The verifier later folds this vector via Lagrange + // interpolation at x3 (block 4). + // + // Storage: + // Q_EVAL_SET_MPTR + 0x20 * (set_offset + k) + // where set_offset is the cumulative |sets[(); + + if q_eval_strategy(commitments_in_set) == QEvalStrategy::Rolled { + // -------- Rolled path (Opt I + Opt J merged) ---------- + lines.push(format!( + "// q_eval_set[{set_idx}]: {m} commitment(s) (rolled, m>={Q_EVAL_ROLL_THRESHOLD})" + )); + + // 1. Pre-stage source-eval addresses at EVAL_SRC_TABLE_MPTR. Layout: row-major + // i over commits, k over rotations. Stride between commit rows = n_rot * + // 0x20. + lines.push("// stage per-(commit, rotation) eval source addresses".to_string()); + for (i, c) in commitments_in_set.iter().enumerate() { + debug_assert_eq!(c.evals.len(), n_rot); + for (k, ev) in c.evals.iter().enumerate() { + debug_assert!(matches!(ev.loc(), Location::Memory)); + lines.push(format!( + "mstore({:#x}, {})", + eval_src_table_mptr + (i * n_rot + k) * WORD_BYTES, + ev.ptr() + )); + } + } + + // 2. Seed q_eval_set_k stack locals from c[0].evals[k] (x1^0 = 1, so no scaling + // needed). + for (k, ev) in first.evals.iter().enumerate() { + lines.push(format!("let q_eval_set_{k} := {ev}")); + } + + // 3. Single Yul `for` loop: accumulate evals using a single x1 power load per + // iteration (vs n_rot loads in the unrolled form). + // + // Loop variables: + // pow_p : ptr into X1_POWERS_MPTR (i*0x20) + // eval_p: ptr into EVAL_SRC_TABLE_MPTR (i*n_rot*0x20) + // + // Per iter: 1 mload(pow), n_rot * (mload + mulmod + + // addmod) for the evals, and 2 ptr-bump adds. Compared + // to the unrolled block this halves the total mload + // count and gives solc-via-ir a single basic block to + // schedule. + let n_rot_stride = n_rot * WORD_BYTES; + lines.push(format!("let pow_p := add(X1_POWERS_MPTR, {WORD_BYTES:#x})")); + lines.push(format!( + "let eval_p := add({:#x}, {:#x})", + eval_src_table_mptr, n_rot_stride + )); + lines.push(format!( + "for {{ let i := 1 }} lt(i, {:#x}) {{ i := add(i, 1) }} {{", + m + )); + lines.push(" let pow := mload(pow_p)".to_string()); + for k in 0..n_rot { + if k == 0 { + lines.push( + " q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(mload(eval_p)), pow, r), r)" + .to_string(), + ); + } else { + lines.push(format!( + " q_eval_set_{k} := addmod(q_eval_set_{k}, mulmod(mload(mload(add(eval_p, {:#x}))), pow, r), r)", + k * WORD_BYTES + )); + } + } + lines.push(format!(" pow_p := add(pow_p, {WORD_BYTES:#x})")); + lines.push(format!(" eval_p := add(eval_p, {:#x})", n_rot_stride)); + lines.push("}".to_string()); + + // 4. Persist q_eval_set[s][k]. + for k in 0..n_rot { + lines.push(format!( + "mstore(add(Q_EVAL_SET_MPTR, {:#x}), q_eval_set_{k})", + (set_eval_offset_words + k) * WORD_BYTES + )); + } + } else { + // -------- Unrolled path (preserved for non-rolled sets) -- + lines.push(format!("// q_eval_set[{set_idx}]: {m} commitment(s)")); + + // Fr-only eval accumulation in stack locals. + for (k, ev) in first.evals.iter().enumerate() { + lines.push(format!("let q_eval_set_{k} := {ev}")); + } + for (i, c) in commitments_in_set.iter().enumerate().skip(1) { + let x1_pow = format!("mload(add(X1_POWERS_MPTR, {:#x}))", i * WORD_BYTES); + for (k, ev) in c.evals.iter().enumerate() { + lines.push(format!( + "q_eval_set_{k} := addmod(q_eval_set_{k}, mulmod({ev}, {x1_pow}, r), r)" + )); + } + } + + // Persist q_eval_set[s][k]. + for k in 0..first.evals.len() { + lines.push(format!( + "mstore(add(Q_EVAL_SET_MPTR, {:#x}), q_eval_set_{k})", + (set_eval_offset_words + k) * WORD_BYTES + )); + } + } + + blocks.push(lines); + let _ = q_eval_base; // not needed at this layer, kept for symmetry. + } + } + + if trace { + // Trace-only block: per-set q_com materialization that re-runs the + // same MSM Block 5 emits, so the trace stream can log each + // intermediate q_com point. In production (`trace == false`) this + // block is fully skipped because the staged data is never read by + // the verifier itself; Block 5 stages its own copy into + // `pcs_final_msm_scratch_mptr`. + let mut lines: Vec = Vec::new(); + let g1_identity = EcPoint::new(Ptr::memory("G1_IDENTITY_MPTR")); + let linearization_comm = data.computed_quotient_comm; + let simple_selector_cols: Vec = meta.simple_selector_cols.iter().copied().collect(); + let linearization_term_count = linearization_term_count(meta); + let trace_scratch = memory.pcs_q_com_trace_scratch_mptr; + lines.push("// materialize per-set q_com inputs for trace logging".to_string()); + for (set_idx, commitments_in_set) in by_set.iter().enumerate() { + let non_identity_terms = commitments_in_set + .iter() + .map(|entry| { + if entry.comm == g1_identity { + 0 + } else if entry.comm == linearization_comm { + linearization_term_count + } else { + 1 + } + }) + .sum::(); + if non_identity_terms == 0 { + lines.push(format!( + "mcopy({trace_scratch:#x}, G1_IDENTITY_MPTR, {G1_BYTES:#x})" + )); + lines.push(format!( + "trace_point({}, {trace_scratch:#x})", + 40000 + set_idx + )); + continue; + } + + let mut pair_idx = 0usize; + for (commitment_idx, c) in commitments_in_set.iter().enumerate() { + if c.comm == g1_identity { + continue; + } + let scalar = if commitment_idx == 0 { + "1".to_string() + } else { + format!( + "mload(add(X1_POWERS_MPTR, {:#x}))", + commitment_idx * WORD_BYTES + ) + }; + if c.comm == linearization_comm { + let lin_query_var = + format!("q_com_lin_query_scalar_{set_idx}_{commitment_idx}"); + let lin_cur_var = format!("q_com_lin_cur_scalar_{set_idx}_{commitment_idx}"); + lines.push(format!("let {lin_query_var} := {scalar}")); + lines.push(format!( + "let {lin_cur_var} := mulmod({lin_query_var}, mload(add(QUOTIENT_MPTR, {WORD_BYTES:#x})), r)" + )); + + for q_idx in 0..meta.num_quotients { + let pair_base = trace_scratch + pair_idx * G1_MSM_PAIR_BYTES; + lines.push(format!( + "mcopy({pair_base:#x}, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, {:#x}), {G1_BYTES:#x})", + q_idx * G1_BYTES + )); + lines.push(format!( + "mstore({:#x}, {lin_cur_var})", + pair_base + G1_BYTES + )); + pair_idx += 1; + if q_idx + 1 != meta.num_quotients { + lines.push(format!( + "{lin_cur_var} := mulmod({lin_cur_var}, mload(QUOTIENT_MPTR), r)" + )); + } + } + + for (sel_idx, col) in simple_selector_cols.iter().copied().enumerate() { + let pair_base = trace_scratch + pair_idx * G1_MSM_PAIR_BYTES; + let selector_scalar = + format!("mload(add(SELECTOR_ACC_MPTR, {:#x}))", sel_idx * WORD_BYTES); + lines.push(format!( + "mcopy({pair_base:#x}, {}, {G1_BYTES:#x})", + data.fixed_comms[col].ptr() + )); + lines.push(format!( + "mstore({:#x}, mulmod({lin_query_var}, {}, r))", + pair_base + G1_BYTES, + selector_scalar + )); + pair_idx += 1; + } + } else { + let pair_base = trace_scratch + pair_idx * G1_MSM_PAIR_BYTES; + lines.push(format!( + "mcopy({pair_base:#x}, {}, {G1_BYTES:#x})", + c.comm.ptr() + )); + lines.push(format!("mstore({:#x}, {scalar})", pair_base + G1_BYTES)); + pair_idx += 1; + } + } + assert_eq!( + pair_idx, non_identity_terms, + "q_com trace MSM input term count changed during emission" + ); + let msm_len = non_identity_terms * G1_MSM_PAIR_BYTES; + let msm_gas_cap = layout::precompile::g1msm_gas_cap(msm_len); + lines.push(format!( + "let q_com_trace_ok_{set_idx} := staticcall({msm_gas_cap}, 0x0c, {trace_scratch:#x}, {msm_len:#x}, {trace_scratch:#x}, {G1_BYTES:#x})" + )); + lines.push(format!( + "q_com_trace_ok_{set_idx} := and(q_com_trace_ok_{set_idx}, eq(returndatasize(), {G1_BYTES:#x}))" + )); + lines.push(format!( + "if iszero(q_com_trace_ok_{set_idx}) {{ mstore(TRACE_U256_MPTR, {}) revert(TRACE_U256_MPTR, {WORD_BYTES:#x}) }}", + 40000 + set_idx + )); + lines.push(format!( + "trace_point({}, {trace_scratch:#x})", + 40000 + set_idx + )); + } + blocks.push(lines); + } + + // ------------------------------------------------------------------ + // Block 4: f_eval via Horner over reverse(point_sets) using Lagrange + // interpolation at x3. + // Sample a challenge x_3 so the verifier can check that f(X) + // is correctly committed by evaluating it at a fresh point. The expected + // eval uses the prover-supplied q_eval scalar, the reconstructed + // interpolation r_eval, and prod_i (x_3 - point_i). + // + // acc <- 0 + // for s in (n_sets - 1).. down to 0: + // points := point_sets[s] + // evals := q_eval_set[s][0..|points|] + // proofE := mload(Q_EVAL_CPTR + s * 0x20) (= q_evals[s]) + // r_eval := lagrange_interpolate(points, evals).eval(x3) + // den := prod_{p in points} (x3 - p) + // eval := (proofE - r_eval) * den.invert() + // acc := acc * x2 + eval + // f_eval := acc + // + // Stored at F_EVAL_MPTR. + // ------------------------------------------------------------------ + { + let mut lines: Vec = Vec::new(); + lines.push(format!( + "// f_eval via Horner over {n_sets} reversed set(s)" + )); + lines.push("let x2 := mload(X2_MPTR)".to_string()); + lines.push("let x3 := mload(X3_MPTR)".to_string()); + lines.push("let f_eval := 0".to_string()); + // Resolve the calldata pointer to the q_evals block once. + lines.push("let Q_EVAL_CPTR := mload(Q_EVAL_CPTR_MPTR)".to_string()); + // Hoist all distinct rotation points to stack locals; each + // gets read up to ~m^2 times across the set's dx_j and + // lbasis_j chains, so a single mload at the top of the block + // saves several mloads per reference. + for (i, _rot) in distinct_rotations.iter().enumerate() { + lines.push(format!( + "let rot_pt_{i} := mload(add(ROT_POINTS_MPTR, {:#x}))", + i * WORD_BYTES + )); + } + + // Helper closure: rotation-point reference for codegen. + let rot_pt_ref = |rot: i32| -> String { + let idx = distinct_rotations + .iter() + .position(|r| *r == rot) + .expect("rotation present in distinct_rotations"); + format!("rot_pt_{idx}") + }; + + for set_idx in (0..n_sets).rev() { + let points = &sets.point_sets[set_idx]; + let m = points.len(); + // proof_eval is the s-th q_eval scalar in calldata. The + // Solidity proof shim rewrites q_evals to canonical BE words, so + // calldataload gives the field element directly. + let proof_eval = format!( + "calldataload(add(Q_EVAL_CPTR, {:#x}))", + set_idx * WORD_BYTES + ); + // Reference to q_eval_set[set_idx][k]: + let set_eval_offset_words: usize = + sets.point_sets[..set_idx].iter().map(|s| s.len()).sum::(); + + lines.push(format!("// --- set {set_idx} (cardinality {m}) ---")); + lines.push("{".to_string()); // local block + + // Compute den_j = prod_{k != j} (points[j] - points[k]) and + // dx_j = (x3 - points[j]) for each j. + // Then r_eval = sum_j evals[j] * den * inv(dx_j) * inv(den_j), + // where den = prod_j dx_j. + // + // Special-case |set| == 1: r_eval = evals[0], den = dx[0]. + if m == 1 { + let pt = rot_pt_ref(points[0]); + let ev = format!( + "mload(add(Q_EVAL_SET_MPTR, {:#x}))", + set_eval_offset_words * WORD_BYTES + ); + lines.push(format!("let dx0 := addmod(x3, sub(r, {pt}), r)")); + lines.push("let dx0_inv := scalar_inv(dx0)".to_string()); + lines.push(format!( + "let eval := mulmod(addmod({proof_eval}, sub(r, {ev}), r), dx0_inv, r)" + )); + lines.push("f_eval := addmod(mulmod(f_eval, x2, r), eval, r)".to_string()); + lines.push("}".to_string()); + continue; + } + + // General case: m >= 2. + // + // We need to invert {dx_j} for j=0..m and {lbasis_j} for + // j=0..m, where: + // + // dx_j = x3 - p_j + // lbasis_j = prod_{k != j} (p_j - p_k) + // + // Naively that's 2m + 1 separate `scalar_inv` (modexp) calls + // (the original code also inverted `den = prod_j dx_j`). + // But `den_inv` is just `prod_j dx_j_inv`, so we don't need + // to invert it separately. And the remaining 2m values can + // be Montgomery-batched into a SINGLE modexp: + // + // p_0 = a_0 + // p_i = p_{i-1} * a_i for i = 1..n-1 (n-1 muls) + // q = scalar_inv(p_{n-1}) (1 modexp) + // a_inv_i = q * p_{i-1} ; q *= a_i for i = n-1..1 (2(n-1) muls) + // a_inv_0 = q + // + // Total: 1 modexp + (3n − 3) muls vs the original n modexp. + // For m=3, n=6: saves 5 modexp ≈ 7.5 kg. For m=2, n=4: + // saves 3 modexp ≈ 4.5 kg. + // + // Soundness: requires every input to be non-zero. dx_j is + // non-zero by Fiat-Shamir (x3 is uniform random; the + // probability that x3 = p_j for a structured rotation point + // is ~2^-256). lbasis_j is non-zero because the points in a + // set are distinct by construction (`construct_intermediate_sets` + // de-duplicates rotations within each set). Defensive note: + // a malicious prover cannot influence either, so we don't + // need an explicit zero check. + // + // The reference computes lagrange interpolation directly via + // full polynomial construction; here we collapse the + // evaluation at x3 directly using the identity above. + for (j, point) in points.iter().enumerate().take(m) { + let pt = rot_pt_ref(*point); + lines.push(format!("let dx_{j} := addmod(x3, sub(r, {pt}), r)")); + } + // For each j: lagrange_basis_inv_j = inv(prod_{k!=j} (p_j - p_k)) + for (j, point_j) in points.iter().enumerate().take(m) { + let pj = rot_pt_ref(*point_j); + lines.push(format!("let lbasis_{j} := 1")); + for (k, point_k) in points.iter().enumerate().take(m) { + if k == j { + continue; + } + let pk = rot_pt_ref(*point_k); + lines.push(format!( + "lbasis_{j} := mulmod(lbasis_{j}, addmod({pj}, sub(r, {pk}), r), r)" + )); + } + } + + // Build the Montgomery batch input list: dx_0, ..., dx_{m-1}, + // then lbasis_0, ..., lbasis_{m-1}. + let mut batch_inputs: Vec = Vec::with_capacity(2 * m); + for j in 0..m { + batch_inputs.push(format!("dx_{j}")); + } + for j in 0..m { + batch_inputs.push(format!("lbasis_{j}")); + } + let n = batch_inputs.len(); + + // Forward pass: build prefix products bp_0, bp_1, ..., bp_{n-1}. + // bp_0 = batch_inputs[0] + // bp_i = bp_{i-1} * batch_inputs[i] + // Last one (bp_{n-1}) is the total product. + lines.push(format!("let bp_0 := {}", batch_inputs[0])); + for (i, batch_input) in batch_inputs.iter().enumerate().take(n).skip(1) { + lines.push(format!( + "let bp_{i} := mulmod(bp_{}, {}, r)", + i - 1, + batch_input + )); + } + + // One modexp for the whole set. + lines.push(format!("let bq := scalar_inv(bp_{})", n - 1)); + + // Backward pass: extract individual inverses. Walk from + // i=n-1 down to i=1, then handle i=0 last. + // + // inv_i = bq * bp_{i-1} + // bq *= batch_inputs[i] + // + // We name each inverse using its original variable: the + // first m inputs are dx_j → dx_inv_j, the next m are + // lbasis_j → lbasis_inv_j. + let inv_name = |idx: usize| -> String { + if idx < m { + format!("dx_inv_{idx}") + } else { + format!("lbasis_inv_{}", idx - m) + } + }; + for i in (1..n).rev() { + lines.push(format!( + "let {} := mulmod(bq, bp_{}, r)", + inv_name(i), + i - 1 + )); + lines.push(format!("bq := mulmod(bq, {}, r)", batch_inputs[i])); + } + lines.push(format!("let {} := bq", inv_name(0))); + + // den_inv = prod_j dx_inv_j (free, no extra modexp). + lines.push("let den_inv := dx_inv_0".to_string()); + for j in 1..m { + lines.push(format!("den_inv := mulmod(den_inv, dx_inv_{j}, r)")); + } + + // r_eval = sum_j evals[j] * den * inv(dx_j) * lbasis_inv_j + // = den * sum_j evals[j] * inv(dx_j) * lbasis_inv_j + // + // We can simplify: (proof_eval - r_eval) * inv(den) + // = proof_eval * inv(den) - sum_j evals[j] * inv(dx_j) * lbasis_inv_j + lines.push(format!("let eval := mulmod({proof_eval}, den_inv, r)")); + for j in 0..m { + let ev_j = format!( + "mload(add(Q_EVAL_SET_MPTR, {:#x}))", + (set_eval_offset_words + j) * WORD_BYTES + ); + lines.push(format!( + "let term_{j} := mulmod(mulmod({ev_j}, dx_inv_{j}, r), lbasis_inv_{j}, r)" + )); + lines.push(format!("eval := addmod(eval, sub(r, term_{j}), r)")); + } + lines.push("f_eval := addmod(mulmod(f_eval, x2, r), eval, r)".to_string()); + lines.push("}".to_string()); + } + + lines.push("mstore(F_EVAL_MPTR, f_eval)".to_string()); + blocks.push(lines); + } + + // ------------------------------------------------------------------ + // Block 5: fused final commitment MSM with x4 powers. + // + // Instead of materialising every q_com[s] and then combining those + // points with x4 powers, use linearity: + // + // q_com[s] = sum_i x1^i * C[s][i] + // final_com = sum_s x4^s * q_com[s] + x4^n_sets * f_com + // = sum_{s,i} (x4^s * x1^i) * C[s][i] + // + x4^n_sets * f_com + // + // The q_eval_set scalar folds from Block 3 are still needed by Block 4, + // but q_com points are not needed anywhere else. This also lets us skip + // G1 identity commitments in the MSM input while preserving their eval + // contribution in q_eval_set. + // ------------------------------------------------------------------ + { + let mut lines: Vec = Vec::new(); + let g1_identity = EcPoint::new(Ptr::memory("G1_IDENTITY_MPTR")); + let linearization_comm = data.computed_quotient_comm; + let simple_selector_cols: Vec = meta.simple_selector_cols.iter().copied().collect(); + let final_msm_shape = final_msm_shape(meta, data, &by_set); + let final_msm_terms = final_msm_shape.terms; + let final_msm_len = final_msm_shape.input_bytes; + let final_msm_scratch = memory.pcs_final_msm_scratch_mptr; + + lines.push("// build final_com and v (KZG single-opening proof, fused MSM)".to_string()); + lines.push(format!( + "// final MSM input length from circuit/VK shape: {final_msm_terms} term(s)" + )); + lines.push("let x4 := mload(X4_MPTR)".to_string()); + lines.push("let lin_x_split := mload(QUOTIENT_MPTR)".to_string()); + lines.push(format!( + "let lin_one_minus_x_n := mload(add(QUOTIENT_MPTR, {WORD_BYTES:#x}))" + )); + // Resolve the calldata pointer to the q_evals block once. + lines.push("let Q_EVAL_CPTR := mload(Q_EVAL_CPTR_MPTR)".to_string()); + + // truncated-challenges: midnight-proofs + // proofs/src/poly/kzg/mod.rs uses + // truncated_powers(x4)[i] = truncate(x4^i) + // i.e. the internal accumulator stays full precision while + // each emitted power is truncated to 128 bits. + if truncated_challenges { + lines.push("let x4_pow_full := 1".to_string()); + } else { + lines.push("let x4_pow_0 := 1".to_string()); + } + for s in 1..=n_sets { + if truncated_challenges { + lines.push("x4_pow_full := mulmod(x4_pow_full, x4, r)".to_string()); + lines.push(format!( + "let x4_pow_{s} := and(x4_pow_full, {TRUNC_MASK_128})" + )); + } else { + lines.push(format!("let x4_pow_{s} := mulmod(x4_pow_{}, x4, r)", s - 1)); + } + } + + // v = sum_s x4^s * q_evals[s] + x4^n_sets * f_eval. + lines.push("let v := calldataload(Q_EVAL_CPTR)".to_string()); + for s in 1..n_sets { + lines.push(format!( + "v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, {:#x})), x4_pow_{s}, r), r)", + s * WORD_BYTES + )); + } + lines.push(format!( + "v := addmod(v, mulmod(mload(F_EVAL_MPTR), x4_pow_{n_sets}, r), r)" + )); + + // final_com = sum_{s,i} (x4^s * x1^i) * C[s][i] + // + x4^n_sets * f_com. + let mut pair_idx = 0usize; + let scalar_expr = |set_idx: usize, commitment_idx: usize| -> String { + let x1_pow = if commitment_idx == 0 { + "1".to_string() + } else { + format!( + "mload(add(X1_POWERS_MPTR, {:#x}))", + commitment_idx * WORD_BYTES + ) + }; + + match (set_idx, commitment_idx) { + (0, _) => x1_pow, + (_, 0) => format!("x4_pow_{set_idx}"), + _ => format!("mulmod({x1_pow}, x4_pow_{set_idx}, r)"), + } + }; + + for (set_idx, commitments_in_set) in by_set.iter().enumerate() { + for (commitment_idx, c) in commitments_in_set.iter().enumerate() { + if c.comm == g1_identity { + continue; + } + + let scalar = scalar_expr(set_idx, commitment_idx); + if c.comm == linearization_comm { + let lin_query_var = format!("lin_query_scalar_{pair_idx}"); + let lin_cur_var = format!("lin_cur_scalar_{pair_idx}"); + lines.push(format!("let {lin_query_var} := {scalar}")); + lines.push(format!( + "let {lin_cur_var} := mulmod({lin_query_var}, lin_one_minus_x_n, r)" + )); + + for q_idx in 0..meta.num_quotients { + let pair_base = final_msm_scratch + pair_idx * G1_MSM_PAIR_BYTES; + lines.push(format!( + "mcopy({pair_base:#x}, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, {:#x}), {G1_BYTES:#x})", + q_idx * G1_BYTES + )); + lines.push(format!( + "mstore({:#x}, {lin_cur_var})", + pair_base + G1_BYTES, + )); + pair_idx += 1; + if q_idx + 1 != meta.num_quotients { + lines.push(format!( + "{lin_cur_var} := mulmod({lin_cur_var}, lin_x_split, r)" + )); + } + } + + for (sel_idx, col) in simple_selector_cols.iter().copied().enumerate() { + let pair_base = final_msm_scratch + pair_idx * G1_MSM_PAIR_BYTES; + let selector_scalar = + format!("mload(add(SELECTOR_ACC_MPTR, {:#x}))", sel_idx * WORD_BYTES); + lines.push(format!( + "mcopy({pair_base:#x}, {}, {G1_BYTES:#x})", + data.fixed_comms[col].ptr() + )); + lines.push(format!( + "mstore({:#x}, mulmod({lin_query_var}, {}, r))", + pair_base + G1_BYTES, + selector_scalar + )); + pair_idx += 1; + } + } else { + let pair_base = final_msm_scratch + pair_idx * G1_MSM_PAIR_BYTES; + lines.push(format!( + "mcopy({pair_base:#x}, {}, {G1_BYTES:#x})", + c.comm.ptr() + )); + lines.push(format!("mstore({:#x}, {scalar})", pair_base + G1_BYTES)); + pair_idx += 1; + } + } + } + + let pair_base = final_msm_scratch + pair_idx * G1_MSM_PAIR_BYTES; + lines.push(format!("mcopy({pair_base:#x}, F_COM_MPTR, {G1_BYTES:#x})")); + lines.push(format!( + "mstore({:#x}, x4_pow_{n_sets})", + pair_base + G1_BYTES + )); + pair_idx += 1; + assert_eq!( + pair_idx, final_msm_terms, + "final MSM input term count changed during emission" + ); + + let final_msm_gas_cap = layout::precompile::g1msm_gas_cap(final_msm_len); + lines.push("if success {".to_string()); + lines.push(format!( + " success := staticcall({final_msm_gas_cap}, 0x0c, {final_msm_scratch:#x}, {:#x}, FINAL_COM_MPTR, {G1_BYTES:#x})", + final_msm_len + )); + lines.push(format!( + " success := and(success, eq(returndatasize(), {G1_BYTES:#x}))" + )); + lines.push("}".to_string()); + lines.push("mstore(V_MPTR, v)".to_string()); + + blocks.push(lines); + } + + // ------------------------------------------------------------------ + // Block 6: pairing inputs. + // + // PAIRING_LHS = pi + // PAIRING_RHS = final_com - v*G + x3*pi + // + // We compute RHS in three steps: + // tmp1 := -v * G (G is G1 generator at G1_BASE_MPTR) + // tmp2 := x3 * pi + // PAIRING_RHS := final_com + tmp1 + tmp2 + // + // Note: We tried batching this into a 3-pair MSM but per EIP-2537 + // gas tables, 3-pair G1MSM (~27.5k gas) is *more* expensive than + // 2 × G1MSM-1 + 2 × G1ADD (= 25k gas) in this size regime; the + // multi-pair discount only starts to dominate at >= 4 pairs. + // ------------------------------------------------------------------ + { + let mut lines: Vec = Vec::new(); + lines.push("// Scale z*pi - vG before the final pairing check".to_string()); + lines.push("// pairing inputs (LHS = pi; RHS = final_com - v*G + x3*pi)".to_string()); + + // PAIRING_LHS = pi (paired against G2_BASE). + lines.push(format!("mcopy(PAIRING_LHS_MPTR, PI_MPTR, {G1_BYTES:#x})")); + + // tmp = (-v) * G => load G into planned scratch, scale by (r - v). + let scratch = memory.pcs_pairing_scratch_mptr; + let scratch_g1_b = scratch + G1_BYTES; + let scratch_g1add_scalar = scratch + G1ADD_INPUT_BYTES; + lines.push(format!("mcopy({scratch:#x}, G1_BASE_MPTR, {G1_BYTES:#x})")); + lines.push(format!( + "mstore({scratch_g1_b:#x}, addmod(0, sub(r, mload(V_MPTR)), r))" + )); + lines.push("if success {".to_string()); + lines.push(format!( + " success := staticcall({}, 0x0c, {scratch:#x}, {G1_MSM_PAIR_BYTES:#x}, {scratch:#x}, {G1_BYTES:#x})", + layout::precompile::g1msm_gas_cap(G1_MSM_PAIR_BYTES) + )); + lines.push(format!( + " success := and(success, eq(returndatasize(), {G1_BYTES:#x}))" + )); + lines.push("}".to_string()); + + // tmp += final_com. + lines.push(format!( + "mcopy({scratch_g1_b:#x}, FINAL_COM_MPTR, {G1_BYTES:#x})" + )); + lines.push("if success {".to_string()); + lines.push(format!( + " success := staticcall({}, 0x0b, {scratch:#x}, {G1ADD_INPUT_BYTES:#x}, {scratch:#x}, {G1_BYTES:#x})", + layout::precompile::G1ADD_GAS_CAP + )); + lines.push(format!( + " success := and(success, eq(returndatasize(), {G1_BYTES:#x}))" + )); + lines.push("}".to_string()); + + // tmp += x3 * pi. + lines.push(format!("mcopy({scratch_g1_b:#x}, PI_MPTR, {G1_BYTES:#x})")); + lines.push(format!("mstore({scratch_g1add_scalar:#x}, mload(X3_MPTR))")); + lines.push("if success {".to_string()); + lines.push(format!( + " success := staticcall({}, 0x0c, {scratch_g1_b:#x}, {G1_MSM_PAIR_BYTES:#x}, {scratch_g1_b:#x}, {G1_BYTES:#x})", + layout::precompile::g1msm_gas_cap(G1_MSM_PAIR_BYTES) + )); + lines.push(format!( + " success := and(success, eq(returndatasize(), {G1_BYTES:#x}))" + )); + lines.push("}".to_string()); + lines.push("if success {".to_string()); + lines.push(format!( + " success := staticcall({}, 0x0b, {scratch:#x}, {G1ADD_INPUT_BYTES:#x}, {scratch:#x}, {G1_BYTES:#x})", + layout::precompile::G1ADD_GAS_CAP + )); + lines.push(format!( + " success := and(success, eq(returndatasize(), {G1_BYTES:#x}))" + )); + lines.push("}".to_string()); + + // Persist as PAIRING_RHS = final_com - v*G + x3*pi. + lines.push(format!( + "mcopy(PAIRING_RHS_MPTR, {scratch:#x}, {G1_BYTES:#x})" + )); + + blocks.push(lines); + } + + blocks +} + +// --------------------------------------------------------------------------- +// Diagnostics tests against a hand-built minimal `IntermediateSets`. +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use crate::lowering::encoding::{EcPoint, Ptr}; + + /// Memory-backed G1 test handle. + fn pt(off: usize) -> EcPoint { + EcPoint::new(Ptr::memory(off)) + } + /// Memory-backed scalar test handle. + fn ev(off: usize) -> Word { + Word::from(Ptr::memory(off)) + } + /// Calldata-backed scalar test handle. + fn calldata_ev(off: usize) -> Word { + Word::from(Ptr::calldata(off)) + } + + // These tests exercise the small set-cover helper used by the generator + // invariant. `absorbed` models proof G1s that `common_uncompressed_g1` + // feeds into Fiat-Shamir before an independent curve check; `precompile_inputs` + // models the later EIP-2537 G1MSM/pairing inputs that provide the + // subgroup-validating boundary. The concrete generator path builds both + // lists from memory handles, but synthetic points keep the failure mode + // tight and readable here. + #[test] + fn absorbed_g1_coverage_accepts_later_precompile_input() { + let absorbed = vec![ + TrackedG1::new("advice[0]", pt(0x100)), + TrackedG1::new("pi", pt(0x200)), + ]; + let precompile_inputs = vec![ + TrackedG1::new("advice[0] via final PCS G1MSM", pt(0x100)), + TrackedG1::new("pi via final pairing input", pt(0x200)), + ]; + + validate_g1_precompile_coverage(&absorbed, &precompile_inputs).unwrap(); + } + + #[test] + fn absorbed_g1_coverage_rejects_point_without_precompile_input() { + let absorbed = vec![ + TrackedG1::new("advice[0]", pt(0x100)), + TrackedG1::new("pi", pt(0x200)), + ]; + let precompile_inputs = vec![TrackedG1::new("pi via final pairing input", pt(0x200))]; + + let err = validate_g1_precompile_coverage(&absorbed, &precompile_inputs).unwrap_err(); + assert!(err.contains("advice[0]")); + assert!(err.contains("EIP-2537 G1MSM/pairing precompile")); + } + + #[test] + fn q_eval_roll_strategy_is_single_source_for_planning_and_emission() { + assert_eq!(Q_EVAL_ROLL_THRESHOLD, 4); + + let rolled = vec![ + CommitmentEntry { + set_index: 0, + comm: pt(0x100), + evals: vec![ev(0x200), ev(0x220)], + }, + CommitmentEntry { + set_index: 0, + comm: pt(0x180), + evals: vec![ev(0x240), ev(0x260)], + }, + CommitmentEntry { + set_index: 0, + comm: pt(0x200), + evals: vec![ev(0x280), ev(0x2a0)], + }, + CommitmentEntry { + set_index: 0, + comm: pt(0x280), + evals: vec![ev(0x2c0), ev(0x2e0)], + }, + ]; + let refs = rolled.iter().collect::>(); + assert_eq!(q_eval_strategy(&refs), QEvalStrategy::Rolled); + + let mut calldata_backed = rolled.clone(); + calldata_backed[3].evals[1] = calldata_ev(0x40); + let refs = calldata_backed.iter().collect::>(); + assert_eq!(q_eval_strategy(&refs), QEvalStrategy::Unrolled); + + let mut ragged = rolled; + ragged[3].evals.pop(); + let refs = ragged.iter().collect::>(); + assert_eq!(q_eval_strategy(&refs), QEvalStrategy::Unrolled); + } + + #[test] + fn intermediate_sets_partitions_by_rotation_set() { + // Build a synthetic query list: + // c0 at rot 0 + // c1 at rot 0, rot 1 + // c2 at rot 0, rot 1 + // c3 at rot 0, rot 1, rot -1 + // + // Expected: + // set0 = {0} -> c0 + // set1 = {0, 1} -> c1, c2 + // set2 = {-1, 0, 1} -> c3 + let queries = vec![ + Query::new(pt(0x100), 0, ev(0x200)), + Query::new(pt(0x110), 0, ev(0x210)), + Query::new(pt(0x110), 1, ev(0x230)), + Query::new(pt(0x120), 0, ev(0x240)), + Query::new(pt(0x120), 1, ev(0x260)), + Query::new(pt(0x130), 0, ev(0x270)), + Query::new(pt(0x130), 1, ev(0x290)), + Query::new(pt(0x130), -1, ev(0x2b0)), + ]; + + let raw = construct_intermediate_sets_impl(&queries); + // After dedup, point_sets must include {0}, {0, 1}, {-1, 0, 1}. + let mut found_sizes: Vec = raw.point_sets.iter().map(|s| s.len()).collect(); + found_sizes.sort(); + assert_eq!(found_sizes, vec![1, 2, 3]); + + // Now sort and verify ordering and content. + let sorted = sort_sets(raw); + assert_eq!( + sorted.point_sets.iter().map(|s| s.len()).collect::>(), + vec![1, 2, 3] + ); + // Sets sorted by ascending cardinality must hold the expected + // rotation values, regardless of the within-set order. Compare + // by sorted content to avoid coupling the test to the internal + // `point_index_of` ordering. + let mut s0 = sorted.point_sets[0].clone(); + s0.sort_unstable(); + assert_eq!(s0, vec![0]); + let mut s1 = sorted.point_sets[1].clone(); + s1.sort_unstable(); + assert_eq!(s1, vec![0, 1]); + let mut s2 = sorted.point_sets[2].clone(); + s2.sort_unstable(); + assert_eq!(s2, vec![-1, 0, 1]); + + // Eval alignment: each commitment's `evals[k]` must correspond to + // the rotation at `sorted.point_sets[c.set_index][k]`. + let mut found_c0 = false; + let mut found_c3 = false; + for c in &sorted.commitments { + let pts = &sorted.point_sets[c.set_index]; + assert_eq!(c.evals.len(), pts.len(), "evals/points length mismatch"); + if c.comm == pt(0x100) { + found_c0 = true; + assert_eq!(c.evals, vec![ev(0x200)]); + } + if c.comm == pt(0x130) { + found_c3 = true; + // c3's evals are aligned with the set's sorted point list; + // use the protocol-level pairs (rotation -> eval) for a + // location-independent check. + let pairs: Vec<(i32, Word)> = + pts.iter().copied().zip(c.evals.iter().copied()).collect(); + assert!(pairs.contains(&(0, ev(0x270)))); + assert!(pairs.contains(&(1, ev(0x290)))); + assert!(pairs.contains(&(-1, ev(0x2b0)))); + } + } + assert!( + found_c0 && found_c3, + "expected c0 and c3 entries in commitments" + ); + } + + #[test] + fn intermediate_sets_dedups_commitments() { + // Two queries with the same commitment at two different + // rotations must coalesce into one CommitmentEntry with two + // evals. + let queries = vec![ + Query::new(pt(0x100), 0, ev(0x200)), + Query::new(pt(0x100), 1, ev(0x220)), + ]; + let result = sort_sets(construct_intermediate_sets_impl(&queries)); + assert_eq!(result.commitments.len(), 1); + assert_eq!(result.commitments[0].evals.len(), 2); + assert_eq!(result.point_sets.len(), 1); + assert_eq!(result.point_sets[0].len(), 2); + } + + // ---------------------------------------------------------------- + // Phase 3: dummy-query computation (fewer-point-sets path). + // ---------------------------------------------------------------- + + #[test] + fn compute_dummy_queries_emits_no_dummies_when_all_singletons() { + // Every commitment has exactly one rotation - no point sets to + // unify. Output must be empty. + let queries = vec![ + Query::new(pt(0x100), 0, ev(0x200)), + Query::new(pt(0x110), 0, ev(0x220)), + Query::new(pt(0x120), 5, ev(0x240)), + ]; + assert_eq!(compute_dummy_queries(&queries), Vec::new()); + } + + #[test] + fn compute_dummy_queries_emits_no_dummies_for_aligned_pairs() { + // Two non-singleton commitments share the same rotation set. + // No dummies are needed. + let queries = vec![ + Query::new(pt(0x100), 0, ev(0x200)), + Query::new(pt(0x100), 1, ev(0x210)), + Query::new(pt(0x110), 0, ev(0x220)), + Query::new(pt(0x110), 1, ev(0x230)), + ]; + assert_eq!(compute_dummy_queries(&queries), Vec::new()); + } + + #[test] + fn compute_dummy_queries_unifies_two_distinct_pairs() { + // c1 at {0, 1}, c2 at {0, 2}. Union = {0, 1, 2}; c1 needs a + // dummy at 2; c2 needs a dummy at 1. + let queries = vec![ + Query::new(pt(0x100), 0, ev(0x200)), + Query::new(pt(0x100), 1, ev(0x210)), + Query::new(pt(0x110), 0, ev(0x220)), + Query::new(pt(0x110), 2, ev(0x230)), + ]; + let dummies = compute_dummy_queries(&queries); + // c1's first occurrence is index 0; c2's first occurrence is 2. + // Insertion order of union: rot 0, rot 1, rot 2 (from c1 first, + // then c2 contributes 2). For c1 (existing {0, 1}), missing 2. + // For c2 (existing {0, 2}), missing 1. + assert_eq!( + dummies, + vec![ + DummyQuery { + query_index: 0, + rotation: 2, + }, + DummyQuery { + query_index: 2, + rotation: 1, + }, + ] + ); + } + + #[test] + fn compute_dummy_queries_matches_midnight_singleton_padding() { + // This mirrors the current midnight-proofs helper exactly: + // singleton groups do not contribute points to the union, but + // once the union is known they are still padded to it. + let queries = vec![ + Query::new(pt(0x0f0), 0, ev(0x100)), // c0 (singleton) + Query::new(pt(0x100), 0, ev(0x200)), // c1 + Query::new(pt(0x100), 1, ev(0x210)), + Query::new(pt(0x110), 0, ev(0x220)), // c2 + Query::new(pt(0x110), 2, ev(0x230)), + ]; + let dummies = compute_dummy_queries(&queries); + assert_eq!( + dummies, + vec![ + DummyQuery { + query_index: 0, // c0's singleton gets padded after unioning c1/c2 + rotation: 1, + }, + DummyQuery { + query_index: 0, + rotation: 2, + }, + DummyQuery { + query_index: 1, // c1's first occurrence + rotation: 2, + }, + DummyQuery { + query_index: 3, // c2's first occurrence + rotation: 1, + }, + ] + ); + } + + #[test] + fn augmented_queries_collapse_to_single_set() { + // After adding dummies, the augmented query list must yield + // exactly ONE non-singleton point set covering {0, 1, 2}. + let raw = vec![ + Query::new(pt(0x100), 0, ev(0x200)), + Query::new(pt(0x100), 1, ev(0x210)), + Query::new(pt(0x110), 0, ev(0x220)), + Query::new(pt(0x110), 2, ev(0x230)), + ]; + let dummy_words = vec![ev(0x300), ev(0x320)]; + let augmented = augment_queries_with_dummies(&raw, &dummy_words); + assert_eq!(augmented.len(), 6); + let sets = sort_sets(construct_intermediate_sets_impl(&augmented)); + assert_eq!(sets.point_sets.len(), 1, "all merged into one set"); + assert_eq!(sets.point_sets[0].len(), 3); + } +} diff --git a/proofs/solidity-verifier/src/lowering/layout/memory.rs b/proofs/solidity-verifier/src/lowering/layout/memory.rs new file mode 100644 index 000000000..30cfe4cd7 --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/layout/memory.rs @@ -0,0 +1,1427 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Generated-verifier memory planner. +//! +//! The Solidity verifier intentionally uses absolute Yul memory addresses +//! instead of Solidity's free-memory pointer. That keeps generated code small, +//! makes precompile frames cheap to address, and lets the external quotient +//! evaluator rehydrate exactly the same verifier memory image. The tradeoff is +//! that memory safety has to be checked at codegen time. +//! +//! `VerifierMemoryLayout` is that codegen-time manifest. It preserves the +//! historical addresses in the generated verifier, gives each range a name and +//! lifetime, and rejects accidental overlap when two live ranges can coexist. +//! Intentional scratch reuse is modeled by assigning the same byte range to +//! disjoint `MemoryPhase`s. +//! +//! This is not a packing allocator yet. The first version is deliberately +//! conservative: it names the old layout, validates it, and centralizes all +//! sizing decisions. See `docs/architecture/MEMORY_LAYOUT.md` for the offset +//! table and the rules for changing it. + +use std::collections::BTreeMap; + +pub(crate) use crate::lowering::layout::{ + ACC_MSM_MIN_SCRATCH_BYTES, G1ADD_INPUT_BYTES, G1_BYTES, G1_MSM_PAIR_BYTES, G1_WORDS, + LOW_MEMORY_SCRATCH_START, MODEXP_FRAME_BYTES, MODEXP_SCRATCH_BYTES, PAIRING_PAIR_BYTES, + PAIRING_STATIC_WORKING_WORDS, PAIRING_TWO_PAIR_BYTES, PCS_PAIRING_SCRATCH_START, + PCS_STATIC_WORKING_WORDS, QUOTIENT_RETURN_BUFFER_START, SOLIDITY_FREE_MEMORY_POINTER_SLOT, + SOLIDITY_RESERVED_MEMORY_BYTES, SOLIDITY_SCRATCH_SPACE_BYTES, SOLIDITY_ZERO_SLOT, + TRANSCRIPT_BUFFER_START, VERIFIER_RETURN_BUFFER_START, VK_CONSTRUCTOR_PAYLOAD_START, + WORD_BYTES, +}; +use crate::lowering::{ + encoding::{ConstraintSystemMeta, Ptr}, + layout::{theta_window, ThetaSlot}, + render::Halo2VerifyingKey, +}; +/// Accumulator pairing-batch hash frame. +/// +/// The template starts this frame at `0x100`, writes a one-word domain tag, +/// then four G1 points: KZG rhs/lhs and accumulator rhs/lhs. The last copy ends +/// at `0x320`, so the registered range is `[0x100, 0x320)`. +const ACCUMULATOR_PAIRING_BATCH_BYTES: usize = + PAIRING_TWO_PAIR_BYTES - G1ADD_INPUT_BYTES + WORD_BYTES; + +// Fixed word offsets from `THETA_MPTR`. +// +// These constants are compatibility anchors for the current generated +// verifier. The range `[THETA_MPTR, THETA_MPTR + 52 words)` holds scalar +// challenges, proof G1 slots, accumulator G1 slots, and pairing-input G1 +// slots. PCS fixed windows start at word 52. Some gaps are intentionally left +// unused because older generated templates had those offsets; they are not +// available for scratch unless registered below with a disjoint lifetime. +// +// words region +// 0..10 theta, beta, gamma, trash_challenge, y, x, x1, x2, x3, x4 +// 10..14 f_com G1 +// 14..18 pi G1 +// 18..22 accumulator lhs G1 +// 22..26 accumulator rhs G1 +// 26..33 Lagrange and linearization scalar slots +// 33..37 quotient linearization scratch, rendered as a trace G1 +// 37 historical padding +// 38..40 f_eval, v +// 40..44 final_com G1 +// 44..48 pairing lhs G1 +// 48..52 pairing rhs G1 +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct ThetaWindowLayout { + pub(crate) rot_points_word: usize, + pub(crate) x1_powers_word: usize, + pub(crate) q_com_word: usize, + pub(crate) q_eval_set_word: usize, + pub(crate) q_eval_cptr_word: usize, + pub(crate) g1_identity_word: usize, + pub(crate) reversed_evals_word: usize, + pub(crate) rot_points_cap_words: usize, + pub(crate) x1_powers_cap_words: usize, + pub(crate) q_com_cap_words: usize, + pub(crate) q_eval_set_cap_words: usize, +} + +impl ThetaWindowLayout { + /// Return the historical theta-rooted window layout. + /// + /// Debug assertions tie the derived offsets back to `layout::theta_window` + /// so future edits cannot drift silently. + pub(crate) fn compatibility() -> Self { + let rot_points_word = ThetaSlot::PairingRhs.word() + G1_WORDS; + let x1_powers_word = rot_points_word + theta_window::ROT_POINTS_CAP_WORDS; + let q_com_word = x1_powers_word + theta_window::X1_POWERS_CAP_WORDS; + let q_eval_set_word = q_com_word; + let q_eval_cptr_word = q_eval_set_word + theta_window::Q_EVAL_SET_CAP_WORDS; + let g1_identity_word = q_eval_cptr_word + 1 + theta_window::Q_EVAL_CPTR_PADDING_WORDS; + let reversed_evals_word = + g1_identity_word + G1_WORDS + theta_window::G1_IDENTITY_PADDING_WORDS; + + debug_assert_eq!(rot_points_word, theta_window::ROT_POINTS_WORD); + debug_assert_eq!(x1_powers_word, theta_window::X1_POWERS_WORD); + debug_assert_eq!(q_com_word, theta_window::Q_COM_WORD); + debug_assert_eq!(q_eval_set_word, theta_window::Q_EVAL_SET_WORD); + debug_assert_eq!(q_eval_cptr_word, theta_window::Q_EVAL_CPTR_WORD); + debug_assert_eq!(g1_identity_word, theta_window::G1_IDENTITY_WORD); + debug_assert_eq!(reversed_evals_word, theta_window::REVERSED_EVALS_WORD); + + Self { + rot_points_word, + x1_powers_word, + q_com_word, + q_eval_set_word, + q_eval_cptr_word, + g1_identity_word, + reversed_evals_word, + rot_points_cap_words: theta_window::ROT_POINTS_CAP_WORDS, + x1_powers_cap_words: theta_window::X1_POWERS_CAP_WORDS, + q_com_cap_words: theta_window::Q_COM_CAP_WORDS, + q_eval_set_cap_words: theta_window::Q_EVAL_SET_CAP_WORDS, + } + } +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub(crate) enum MemoryPhase { + /// Generated verifying-key constructor return payload. + VkConstructorPayload, + /// Constructor-only precompile smoke tests. + ConstructorSmoke, + /// Streaming Fiat-Shamir buffer before generated VK memory is live. + Transcript, + /// Single scalar inversion scratch used by the modexp wrapper. + ScalarInv, + /// Batch inversion for Lagrange denominator terms. + LagrangeBatchInvert, + /// Compact quotient VM temps and stack. + QuotientVm, + /// Historical fixed PCS windows rooted at `ROT_POINTS_MPTR`. + PcsFixed, + /// Source-address table used by the rolled q_eval fold. + PcsQEvalSourceTable, + /// Optional trace-only q_com MSM materialization. + PcsQComTrace, + /// Fused final PCS MSM input buffer. + PcsFinalMsm, + /// Low-memory PCS pairing input helpers. + PcsPairing, + /// Public-accumulator MSM input buffer. + AccumulatorMsm, + /// Public-accumulator pairing-batch hash and two G1 add/MSM frames. + AccumulatorPairingBatch, + /// Final two-pair KZG pairing frame. + FinalPairing, + /// Main verifier return word. + VerifierReturn, + /// Standalone quotient evaluator output frame. + QuotientReturn, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) enum MemoryLifetime { + /// Region can be read after it is written and must never overlap another + /// live region. + Permanent, + /// Region is live only during the named phase. Regions in different phases + /// may reuse the same byte range. + Phase(MemoryPhase), +} + +impl MemoryLifetime { + /// Return whether two lifetimes can be live at the same time. + fn intersects(&self, other: &Self) -> bool { + match (self, other) { + (Self::Permanent, _) | (_, Self::Permanent) => true, + (Self::Phase(lhs), Self::Phase(rhs)) => lhs == rhs, + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct MemoryRegion { + /// Stable human-readable region name used in validation errors. + pub(crate) name: &'static str, + /// Start byte offset in EVM memory. + pub(crate) start: usize, + /// Length in bytes. Zero-length regions are allowed for optional paths. + pub(crate) len: usize, + /// Permanent or phase-bounded lifetime. + pub(crate) lifetime: MemoryLifetime, +} + +impl MemoryRegion { + /// Create a named region at an absolute byte range with a lifetime. + pub(crate) fn new( + name: &'static str, + start: usize, + len: usize, + lifetime: MemoryLifetime, + ) -> Self { + Self { + name, + start, + len, + lifetime, + } + } + + /// End byte offset, exclusive. + fn end(&self) -> usize { + self.start + self.len + } + + /// Return whether two non-empty byte ranges overlap. + fn overlaps(&self, other: &Self) -> bool { + self.len != 0 && other.len != 0 && self.start < other.end() && other.start < self.end() + } +} + +#[derive(Clone, Debug, Default)] +pub(crate) struct MemoryMap { + regions: Vec, +} + +impl MemoryMap { + /// Register a region for later validation. + pub(crate) fn push(&mut self, region: MemoryRegion) { + self.regions.push(region); + } + + /// Find a region by name for tests. + #[cfg(test)] + pub(crate) fn region(&self, name: &str) -> Option<&MemoryRegion> { + self.regions.iter().find(|region| region.name == name) + } + + /// Validate alignment, Solidity reserved-memory rules, and live overlap. + pub(crate) fn validate(&self) -> Result<(), String> { + // All generated Yul uses word-granular `mload`, `mstore`, and + // precompile input lengths. Byte-granular ranges would be a bug, not a + // clever packing opportunity. + for region in &self.regions { + if region.len != 0 && region.start < SOLIDITY_RESERVED_MEMORY_BYTES { + let reserved_name = if region.start < SOLIDITY_SCRATCH_SPACE_BYTES { + "scratch space" + } else if region.start < SOLIDITY_ZERO_SLOT { + debug_assert_eq!( + SOLIDITY_FREE_MEMORY_POINTER_SLOT, + SOLIDITY_SCRATCH_SPACE_BYTES + ); + "free-memory pointer" + } else { + "zero slot" + }; + return Err(format!( + "memory region {} starts at {:#x}, inside Solidity-reserved {reserved_name} memory [0x00..{:#x})", + region.name, region.start, SOLIDITY_RESERVED_MEMORY_BYTES + )); + } + if region.start % WORD_BYTES != 0 { + return Err(format!( + "memory region {} starts at unaligned byte offset {:#x}", + region.name, region.start + )); + } + if region.len % WORD_BYTES != 0 { + return Err(format!( + "memory region {} has unaligned length {:#x}", + region.name, region.len + )); + } + } + + // Permanent regions intersect every phase. Scratch regions intersect + // only when the generator says they are live in the same phase. + for (idx, lhs) in self.regions.iter().enumerate() { + for rhs in self.regions.iter().skip(idx + 1) { + if lhs.overlaps(rhs) && lhs.lifetime.intersects(&rhs.lifetime) { + return Err(format!( + "memory region {} [{:#x}..{:#x}) overlaps {} [{:#x}..{:#x})", + lhs.name, + lhs.start, + lhs.end(), + rhs.name, + rhs.start, + rhs.end() + )); + } + } + } + + Ok(()) + } +} + +#[derive(Clone, Debug, Default)] +pub(crate) struct MemoryArena { + map: MemoryMap, +} + +impl MemoryArena { + /// Register a region at an explicit historical or precompile-required + /// byte address. + pub(crate) fn alloc_fixed( + &mut self, + name: &'static str, + start: usize, + len: usize, + lifetime: MemoryLifetime, + ) -> usize { + self.map.push(MemoryRegion::new(name, start, len, lifetime)); + start + } + + /// Register a region immediately after an existing anchor range, rounded + /// up to the next EVM word. This is the compatibility-mode equivalent of + /// "place this after that", without trying to repack earlier anchors. + pub(crate) fn alloc_after( + &mut self, + name: &'static str, + anchor_start: usize, + anchor_len: usize, + len: usize, + lifetime: MemoryLifetime, + ) -> usize { + let start = (anchor_start + anchor_len).next_multiple_of(WORD_BYTES); + self.alloc_fixed(name, start, len, lifetime) + } + + /// Register fixed-address scratch that is live only during one phase. + pub(crate) fn alloc_phase_scratch( + &mut self, + name: &'static str, + start: usize, + len: usize, + phase: MemoryPhase, + ) -> usize { + self.alloc_fixed(name, start, len, MemoryLifetime::Phase(phase)) + } + + /// Create a phase-aware scratch allocator rooted at `base`. + /// + /// Each phase has its own cursor, so allocations in different phases reuse + /// `base`, while multiple allocations in the same phase advance + /// sequentially. That exactly models the current verifier's intentional + /// scratch aliasing without changing byte addresses. + pub(crate) fn scratch_allocator(&mut self, base: usize) -> ScratchAllocator<'_> { + ScratchAllocator { + arena: self, + base, + cursors: BTreeMap::new(), + } + } + + /// Consume the arena and return its registered map. + pub(crate) fn into_map(self) -> MemoryMap { + self.map + } +} + +/// Phase-aware scratch allocator that reuses a base across disjoint phases. +pub(crate) struct ScratchAllocator<'arena> { + arena: &'arena mut MemoryArena, + base: usize, + cursors: BTreeMap, +} + +impl ScratchAllocator<'_> { + /// Allocate phase-scoped scratch from this allocator's base. + pub(crate) fn alloc_phase_scratch( + &mut self, + name: &'static str, + len: usize, + phase: MemoryPhase, + ) -> usize { + let cursor = self.cursors.entry(phase).or_insert(self.base); + let start = *cursor; + *cursor = (start + len).next_multiple_of(WORD_BYTES); + self.arena.alloc_phase_scratch(name, start, len, phase) + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub(crate) struct FinalMsmShape { + /// Number of `(G1, scalar)` pairs emitted for this MSM. + pub(crate) terms: usize, + /// Total G1MSM input length in bytes. + pub(crate) input_bytes: usize, +} + +impl FinalMsmShape { + /// Build a shape from a number of `(G1, scalar)` pairs. + pub(crate) fn from_terms(terms: usize) -> Self { + Self { + terms, + input_bytes: terms * G1_MSM_PAIR_BYTES, + } + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub(crate) struct PcsMemoryRequirements { + /// Distinct rotation points stored at `ROT_POINTS_MPTR`. + pub(crate) rot_points_words: usize, + /// Powers of `x1` stored at `X1_POWERS_MPTR`. + pub(crate) x1_powers_words: usize, + /// Legacy q_com window. Currently zero because production emission fuses + /// q_com terms directly into final MSM scratch. + pub(crate) q_com_words: usize, + /// Folded q_eval scalars stored at `Q_EVAL_SET_MPTR`. + pub(crate) q_eval_set_words: usize, + /// Rolled q_eval source-address table size. + pub(crate) q_eval_source_table_words: usize, + /// Optional trace MSM used to materialize q_com per point set. + pub(crate) q_com_trace_msm: FinalMsmShape, + /// Production fused final PCS MSM. + pub(crate) final_msm: FinalMsmShape, +} + +#[derive(Clone, Debug)] +pub(crate) struct VerifierMemoryLayout { + pub(crate) map: MemoryMap, + pub(crate) theta_windows: ThetaWindowLayout, + /// Fixed low-memory scratch for constructor EIP-2537 smoke checks. + pub(crate) constructor_smoke_scratch_mptr: usize, + /// Base of the streaming transcript buffer. + pub(crate) transcript_mptr: usize, + /// Low-memory scratch used by the PCS pairing-preparation helper. + pub(crate) pcs_pairing_scratch_mptr: usize, + /// Low-memory output word used by the main verifier. + pub(crate) verifier_return_mptr: usize, + /// Low-memory output frame used by the standalone quotient evaluator. + pub(crate) quotient_return_mptr: usize, + /// Start of the copied or embedded verifying-key payload. + pub(crate) vk_mptr: Ptr, + /// Scratch frame used by the scalar-inversion modexp wrapper. + pub(crate) scalar_inv_scratch_mptr: usize, + /// Start of the variable-length user-challenge block after the VK payload. + pub(crate) challenge_mptr: Ptr, + /// Anchor for every historical fixed verifier slot below. + pub(crate) theta_mptr: Ptr, + pub(crate) beta_mptr: Ptr, + pub(crate) gamma_mptr: Ptr, + pub(crate) trash_challenge_mptr: Ptr, + pub(crate) y_mptr: Ptr, + pub(crate) x_mptr: Ptr, + pub(crate) x1_mptr: Ptr, + pub(crate) x2_mptr: Ptr, + pub(crate) x3_mptr: Ptr, + pub(crate) x4_mptr: Ptr, + pub(crate) f_com_mptr: Ptr, + pub(crate) pi_mptr: Ptr, + pub(crate) acc_lhs_mptr: Ptr, + pub(crate) acc_rhs_mptr: Ptr, + pub(crate) x_n_mptr: Ptr, + pub(crate) x_n_minus_1_inv_mptr: Ptr, + pub(crate) l_last_mptr: Ptr, + pub(crate) l_blind_mptr: Ptr, + pub(crate) l_0_mptr: Ptr, + pub(crate) instance_eval_mptr: Ptr, + pub(crate) quotient_eval_mptr: Ptr, + pub(crate) quotient_mptr: Ptr, + pub(crate) f_eval_mptr: Ptr, + pub(crate) v_mptr: Ptr, + pub(crate) final_com_mptr: Ptr, + pub(crate) pairing_lhs_mptr: Ptr, + pub(crate) pairing_rhs_mptr: Ptr, + pub(crate) rot_points_mptr: Ptr, + pub(crate) x1_powers_mptr: Ptr, + pub(crate) q_com_mptr: Ptr, + pub(crate) q_eval_set_mptr: Ptr, + pub(crate) q_eval_cptr_mptr: Ptr, + pub(crate) g1_identity_mptr: Ptr, + pub(crate) reversed_evals_mptr: Ptr, + pub(crate) comms_mptr_base: Ptr, + pub(crate) advice_comms_mptr_base: Ptr, + pub(crate) lookup_m_comms_mptr_base: Ptr, + pub(crate) perm_z_comms_mptr_base: Ptr, + pub(crate) lookup_helper_comms_mptr_base: Ptr, + pub(crate) lookup_z_comms_mptr_base: Ptr, + pub(crate) trashcan_comms_mptr_base: Ptr, + pub(crate) quotient_limb_comms_mptr_base: Ptr, + /// First byte after all decompressed proof commitments. Selector + /// accumulators are live here during final linearization/final MSM. + pub(crate) selector_acc_mptr: usize, + /// Reuses selector-accumulator bytes during the earlier Lagrange batch + /// inversion phase. + pub(crate) batch_invert_scratch_mptr: usize, + /// First quotient VM temporary. Also the canonical PCS scratch base once + /// selector accumulators are accounted for. + pub(crate) quotient_tmp_mptr: usize, + pub(crate) quotient_stack_mptr: usize, + pub(crate) pcs_q_eval_source_table_mptr: usize, + pub(crate) pcs_q_com_trace_scratch_mptr: usize, + pub(crate) pcs_final_msm_scratch_mptr: usize, + /// Public accumulator MSM buffer, historically floored at 0x7000. + pub(crate) acc_msm_scratch: usize, + /// Low-memory transcript hash frame used to batch the accumulator pairing + /// equation with the KZG pairing equation. + pub(crate) accumulator_pairing_batch_mptr: usize, + /// Final KZG two-pairing precompile input frame. + pub(crate) final_pairing_scratch_mptr: usize, + /// One-word buffer used only to materialize LOG data for trace builds. + /// It is planned after every permanent and scratch region because + /// `trace_u256` may be called between long-lived reads from the VK, + /// decoded evals, and decompressed commitments. + pub(crate) trace_u256_mptr: usize, + pub(crate) pcs: PcsMemoryRequirements, +} + +/// Memory map for the generated verifying-key constructor. +/// +/// The VK contract is a separate deployment/runtime context from the verifier, +/// so its constructor payload cannot be mixed into `VerifierMemoryLayout` +/// without creating fake overlaps with verifier-runtime permanent regions. This +/// small layout still keeps the fixed `0x80` payload pointer registered and +/// validated by the same `MemoryMap` rules. +#[derive(Clone, Debug)] +pub(crate) struct VkConstructorMemoryLayout { + pub(crate) map: MemoryMap, + /// Constructor return-runtime buffer. + pub(crate) payload_mptr: usize, +} + +impl VkConstructorMemoryLayout { + /// Register the fixed constructor runtime buffer for a VK runtime length. + pub(crate) fn new(runtime_len: usize) -> Self { + let mut arena = MemoryArena::default(); + let reserved_len = runtime_len.next_multiple_of(WORD_BYTES); + let payload_mptr = arena.alloc_phase_scratch( + "vk_constructor_payload", + VK_CONSTRUCTOR_PAYLOAD_START, + reserved_len, + MemoryPhase::VkConstructorPayload, + ); + Self { + map: arena.into_map(), + payload_mptr, + } + } + + /// Validate the constructor payload pointer and memory map. + pub(crate) fn validate(&self) -> Result<(), String> { + if self.payload_mptr != VK_CONSTRUCTOR_PAYLOAD_START { + return Err(format!( + "VK constructor payload pointer drifted: got {:#x}, expected {VK_CONSTRUCTOR_PAYLOAD_START:#x}", + self.payload_mptr + )); + } + self.map.validate() + } +} + +#[derive(Clone, Copy, Debug, Default)] +pub(crate) struct VerifierMemoryLayoutConfig { + /// Maximum live transcript-buffer words before a squeeze/reset. + pub(crate) transcript_words: usize, + /// Public instance count, needed to size the Lagrange batch-inversion + /// input range. + pub(crate) num_instances: usize, + /// Compact quotient VM persistent state word count before the operand + /// stack. + pub(crate) quotient_state_words: usize, + /// Maximum compact quotient VM stack/scratch words. Native quotient + /// callbacks may share the VM stack base, so callers must include those + /// callback-specific scratch requirements in this count. + pub(crate) quotient_stack_words: usize, + /// Number of `(G1, scalar)` pairs in the public-accumulator MSM. + pub(crate) acc_msm_terms: usize, + /// PCS fixed-window and scratch requirements computed from the circuit and + /// VK query shape. + pub(crate) pcs: PcsMemoryRequirements, +} + +impl VerifierMemoryLayout { + /// Plan the generated verifier's complete memory map. + /// + /// The function preserves historical permanent addresses first, then + /// registers scratch regions with explicit phases so intentional aliasing + /// is validated instead of accidental. + pub(crate) fn new( + meta: &ConstraintSystemMeta, + vk: &Halo2VerifyingKey, + vk_mptr: Ptr, + config: VerifierMemoryLayoutConfig, + ) -> Self { + let commitments_len = commitment_g1_count(meta) * G1_BYTES; + let selector_len = meta.num_simple_selectors * WORD_BYTES; + let quotient_state_len = config.quotient_state_words * WORD_BYTES; + let quotient_stack_len = config.quotient_stack_words * WORD_BYTES; + let q_eval_source_len = config.pcs.q_eval_source_table_words * WORD_BYTES; + let q_com_trace_len = config.pcs.q_com_trace_msm.input_bytes; + let final_msm_len = config.pcs.final_msm.input_bytes; + let acc_msm_len = config.acc_msm_terms * G1_MSM_PAIR_BYTES; + let batch_invert_len = batch_invert_scratch_bytes(meta, config.num_instances); + let quotient_return_len = (2 + meta.num_simple_selectors) * WORD_BYTES; + + let mut arena = MemoryArena::default(); + let theta_windows = ThetaWindowLayout::compatibility(); + + // Low-memory helpers are phase-scoped because the transcript buffer is + // no longer live once algebra/precompile work begins. + let constructor_smoke_scratch_mptr = arena.alloc_phase_scratch( + "constructor_smoke_scratch", + LOW_MEMORY_SCRATCH_START, + PAIRING_PAIR_BYTES, + MemoryPhase::ConstructorSmoke, + ); + let transcript_mptr = arena.alloc_phase_scratch( + "transcript_buffer", + TRANSCRIPT_BUFFER_START, + config.transcript_words * WORD_BYTES, + MemoryPhase::Transcript, + ); + let pcs_pairing_scratch_mptr = arena.alloc_phase_scratch( + "pcs_pairing_static_working", + PCS_PAIRING_SCRATCH_START, + PCS_STATIC_WORKING_WORDS * WORD_BYTES, + MemoryPhase::PcsPairing, + ); + let verifier_return_mptr = arena.alloc_phase_scratch( + "verifier_return", + VERIFIER_RETURN_BUFFER_START, + WORD_BYTES, + MemoryPhase::VerifierReturn, + ); + let quotient_return_mptr = arena.alloc_phase_scratch( + "quotient_return", + QUOTIENT_RETURN_BUFFER_START, + quotient_return_len, + MemoryPhase::QuotientReturn, + ); + let final_pairing_scratch_mptr = arena.alloc_phase_scratch( + "final_pairing_scratch", + crate::lowering::layout::FINAL_PAIRING_SCRATCH_START, + PAIRING_STATIC_WORKING_WORDS * WORD_BYTES, + MemoryPhase::FinalPairing, + ); + + // VK bytes are copied first, then user-phase challenge slots, then + // the fixed theta-relative region. The caller is responsible for + // choosing a stable `vk_mptr` after proof-shape planning; this keeps + // the transcript buffer below the VK payload. + let vk_start = arena.alloc_fixed( + "vk_payload", + vk_mptr.value().as_usize(), + vk.len(), + MemoryLifetime::Permanent, + ); + let scalar_inv_scratch_mptr = arena.alloc_phase_scratch( + "scalar_inv_scratch", + vk_start.saturating_sub(MODEXP_SCRATCH_BYTES), + MODEXP_FRAME_BYTES, + MemoryPhase::ScalarInv, + ); + let challenge_start = arena.alloc_after( + "challenge_slots", + vk_start, + vk.len(), + meta.challenge_indices.len() * WORD_BYTES, + MemoryLifetime::Permanent, + ); + let theta_start = arena.alloc_after( + "theta_scalar_and_g1_slots", + challenge_start, + meta.challenge_indices.len() * WORD_BYTES, + theta_windows.rot_points_word * WORD_BYTES, + MemoryLifetime::Permanent, + ); + let challenge_mptr = Ptr::memory(challenge_start); + let at_theta = |words: usize| theta_start + words * WORD_BYTES; + let ptr_at_theta = |slot: ThetaSlot| Ptr::memory(at_theta(slot.word())); + let theta_mptr = ptr_at_theta(ThetaSlot::Theta); + + let total_advices: usize = meta.num_user_advices.iter().sum(); + let lookup_helper_chunks_total: usize = meta.lookup_chunks.iter().sum(); + // Commitment memory mirrors the proof read schedule but is stored by + // category. PCS and quotient code take typed bases for each category so + // these offsets are the single source of truth for all later mloads. + let non_quotient_g1s = total_advices + + meta.num_lookups + + meta.num_permutation_zs + + lookup_helper_chunks_total + + meta.num_lookups + + meta.num_trashcans; + let committed_g1s = non_quotient_g1s + meta.num_quotients; + let rot_points_mptr = Ptr::memory(arena.alloc_fixed( + "rot_points", + at_theta(theta_windows.rot_points_word), + config.pcs.rot_points_words * WORD_BYTES, + MemoryLifetime::Phase(MemoryPhase::PcsFixed), + )); + let x1_powers_mptr = Ptr::memory(arena.alloc_fixed( + "x1_powers", + at_theta(theta_windows.x1_powers_word), + config.pcs.x1_powers_words * WORD_BYTES, + MemoryLifetime::Phase(MemoryPhase::PcsFixed), + )); + let q_com_mptr = Ptr::memory(arena.alloc_fixed( + "q_com_fixed_window", + at_theta(theta_windows.q_com_word), + config.pcs.q_com_words * WORD_BYTES, + MemoryLifetime::Phase(MemoryPhase::PcsFixed), + )); + let q_eval_set_mptr = Ptr::memory(arena.alloc_fixed( + "q_eval_set", + at_theta(theta_windows.q_eval_set_word), + config.pcs.q_eval_set_words * WORD_BYTES, + MemoryLifetime::Phase(MemoryPhase::PcsFixed), + )); + let q_eval_cptr_mptr = Ptr::memory(arena.alloc_fixed( + "q_eval_cptr_slot", + at_theta(theta_windows.q_eval_cptr_word), + WORD_BYTES, + MemoryLifetime::Permanent, + )); + let g1_identity_mptr = Ptr::memory(arena.alloc_fixed( + "g1_identity", + at_theta(theta_windows.g1_identity_word), + G1_BYTES, + MemoryLifetime::Permanent, + )); + let reversed_evals_mptr = Ptr::memory(arena.alloc_fixed( + "decoded_evals", + at_theta(theta_windows.reversed_evals_word), + meta.num_evals * WORD_BYTES, + MemoryLifetime::Permanent, + )); + let comms_mptr_base = Ptr::memory(arena.alloc_after( + "decompressed_commitments", + reversed_evals_mptr.value().as_usize(), + meta.num_evals * WORD_BYTES, + commitments_len, + MemoryLifetime::Permanent, + )); + let advice_comms_mptr_base = comms_mptr_base; + let lookup_m_comms_mptr_base = advice_comms_mptr_base + G1_WORDS * total_advices; + let perm_z_comms_mptr_base = lookup_m_comms_mptr_base + G1_WORDS * meta.num_lookups; + let lookup_helper_comms_mptr_base = + perm_z_comms_mptr_base + G1_WORDS * meta.num_permutation_zs; + let lookup_z_comms_mptr_base = + lookup_helper_comms_mptr_base + G1_WORDS * lookup_helper_chunks_total; + let trashcan_comms_mptr_base = lookup_z_comms_mptr_base + G1_WORDS * meta.num_lookups; + let quotient_limb_comms_mptr_base = + trashcan_comms_mptr_base + G1_WORDS * meta.num_trashcans; + + // Decompressed proof commitments are stored contiguously by category. + // Everything after this point is either selector state or scratch. + let after_comms = comms_mptr_base.value().as_usize() + committed_g1s * G1_BYTES; + let selector_acc_mptr = arena.alloc_after( + "selector_accumulators", + comms_mptr_base.value().as_usize(), + commitments_len, + selector_len, + MemoryLifetime::Phase(MemoryPhase::PcsFinalMsm), + ); + let batch_invert_scratch_mptr = { + let mut scratch = arena.scratch_allocator(selector_acc_mptr); + scratch.alloc_phase_scratch( + "batch_invert_scratch", + batch_invert_len, + MemoryPhase::LagrangeBatchInvert, + ) + }; + let quotient_tmp_base = (selector_acc_mptr + selector_len).next_multiple_of(WORD_BYTES); + let (quotient_tmp_mptr, quotient_stack_mptr) = { + let mut scratch = arena.scratch_allocator(quotient_tmp_base); + let quotient_tmp_mptr = scratch.alloc_phase_scratch( + "quotient_state", + quotient_state_len, + MemoryPhase::QuotientVm, + ); + let quotient_stack_mptr = scratch.alloc_phase_scratch( + "quotient_stack", + quotient_stack_len.max(MODEXP_FRAME_BYTES), + MemoryPhase::QuotientVm, + ); + (quotient_tmp_mptr, quotient_stack_mptr) + }; + let pcs_scratch_mptr = quotient_tmp_mptr; + let ( + pcs_q_eval_source_table_mptr, + pcs_q_com_trace_scratch_mptr, + pcs_final_msm_scratch_mptr, + ) = { + let mut scratch = arena.scratch_allocator(pcs_scratch_mptr); + let q_eval = scratch.alloc_phase_scratch( + "pcs_q_eval_source_table", + q_eval_source_len, + MemoryPhase::PcsQEvalSourceTable, + ); + let q_com = scratch.alloc_phase_scratch( + "pcs_q_com_trace_msm", + q_com_trace_len, + MemoryPhase::PcsQComTrace, + ); + let final_msm = scratch.alloc_phase_scratch( + "pcs_final_msm", + final_msm_len, + MemoryPhase::PcsFinalMsm, + ); + (q_eval, q_com, final_msm) + }; + let acc_msm_scratch_base = + after_comms.max(ACC_MSM_MIN_SCRATCH_BYTES).next_multiple_of(WORD_BYTES); + let acc_msm_scratch = { + let mut scratch = arena.scratch_allocator(acc_msm_scratch_base); + scratch.alloc_phase_scratch("accumulator_msm", acc_msm_len, MemoryPhase::AccumulatorMsm) + }; + let accumulator_pairing_batch_mptr = arena.alloc_phase_scratch( + "accumulator_pairing_batch", + crate::lowering::layout::accumulator::PAIRING_BATCH_PTR, + ACCUMULATOR_PAIRING_BATCH_BYTES, + MemoryPhase::AccumulatorPairingBatch, + ); + // Trace logging can occur between otherwise long-lived reads, so its + // one-word scratch is placed after every registered region rather than + // borrowing any phase-specific base. + let trace_u256_mptr = [ + constructor_smoke_scratch_mptr + PAIRING_PAIR_BYTES, + transcript_mptr + config.transcript_words * WORD_BYTES, + pcs_pairing_scratch_mptr + PCS_STATIC_WORKING_WORDS * WORD_BYTES, + verifier_return_mptr + WORD_BYTES, + quotient_return_mptr + quotient_return_len, + final_pairing_scratch_mptr + PAIRING_STATIC_WORKING_WORDS * WORD_BYTES, + scalar_inv_scratch_mptr + MODEXP_FRAME_BYTES, + vk_start + vk.len(), + challenge_start + meta.challenge_indices.len() * WORD_BYTES, + theta_start + theta_windows.rot_points_word * WORD_BYTES, + g1_identity_mptr.value().as_usize() + G1_BYTES, + reversed_evals_mptr.value().as_usize() + meta.num_evals * WORD_BYTES, + comms_mptr_base.value().as_usize() + commitments_len, + selector_acc_mptr + selector_len, + batch_invert_scratch_mptr + batch_invert_len, + quotient_stack_mptr + quotient_stack_len.max(MODEXP_FRAME_BYTES), + pcs_q_eval_source_table_mptr + q_eval_source_len, + pcs_q_com_trace_scratch_mptr + q_com_trace_len, + pcs_final_msm_scratch_mptr + final_msm_len, + acc_msm_scratch + acc_msm_len, + accumulator_pairing_batch_mptr + ACCUMULATOR_PAIRING_BATCH_BYTES, + ] + .into_iter() + .max() + .expect("trace scratch bounds are non-empty") + .next_multiple_of(WORD_BYTES); + arena.alloc_fixed( + "trace_u256_log_word", + trace_u256_mptr, + WORD_BYTES, + MemoryLifetime::Permanent, + ); + + Self { + map: arena.into_map(), + theta_windows, + constructor_smoke_scratch_mptr, + transcript_mptr, + pcs_pairing_scratch_mptr, + verifier_return_mptr, + quotient_return_mptr, + vk_mptr, + scalar_inv_scratch_mptr, + challenge_mptr, + theta_mptr, + beta_mptr: ptr_at_theta(ThetaSlot::Beta), + gamma_mptr: ptr_at_theta(ThetaSlot::Gamma), + trash_challenge_mptr: ptr_at_theta(ThetaSlot::TrashChallenge), + y_mptr: ptr_at_theta(ThetaSlot::Y), + x_mptr: ptr_at_theta(ThetaSlot::X), + x1_mptr: ptr_at_theta(ThetaSlot::X1), + x2_mptr: ptr_at_theta(ThetaSlot::X2), + x3_mptr: ptr_at_theta(ThetaSlot::X3), + x4_mptr: ptr_at_theta(ThetaSlot::X4), + f_com_mptr: ptr_at_theta(ThetaSlot::FCom), + pi_mptr: ptr_at_theta(ThetaSlot::Pi), + acc_lhs_mptr: ptr_at_theta(ThetaSlot::AccLhs), + acc_rhs_mptr: ptr_at_theta(ThetaSlot::AccRhs), + x_n_mptr: ptr_at_theta(ThetaSlot::XN), + x_n_minus_1_inv_mptr: ptr_at_theta(ThetaSlot::XNMinus1Inv), + l_last_mptr: ptr_at_theta(ThetaSlot::LLast), + l_blind_mptr: ptr_at_theta(ThetaSlot::LBlind), + l_0_mptr: ptr_at_theta(ThetaSlot::L0), + instance_eval_mptr: ptr_at_theta(ThetaSlot::InstanceEval), + quotient_eval_mptr: ptr_at_theta(ThetaSlot::QuotientEval), + quotient_mptr: ptr_at_theta(ThetaSlot::Quotient), + f_eval_mptr: ptr_at_theta(ThetaSlot::FEval), + v_mptr: ptr_at_theta(ThetaSlot::V), + final_com_mptr: ptr_at_theta(ThetaSlot::FinalCom), + pairing_lhs_mptr: ptr_at_theta(ThetaSlot::PairingLhs), + pairing_rhs_mptr: ptr_at_theta(ThetaSlot::PairingRhs), + rot_points_mptr, + x1_powers_mptr, + q_com_mptr, + q_eval_set_mptr, + q_eval_cptr_mptr, + g1_identity_mptr, + reversed_evals_mptr, + comms_mptr_base, + advice_comms_mptr_base, + lookup_m_comms_mptr_base, + perm_z_comms_mptr_base, + lookup_helper_comms_mptr_base, + lookup_z_comms_mptr_base, + trashcan_comms_mptr_base, + quotient_limb_comms_mptr_base, + selector_acc_mptr, + batch_invert_scratch_mptr, + quotient_tmp_mptr, + quotient_stack_mptr, + pcs_q_eval_source_table_mptr, + pcs_q_com_trace_scratch_mptr, + pcs_final_msm_scratch_mptr, + acc_msm_scratch, + accumulator_pairing_batch_mptr, + final_pairing_scratch_mptr, + trace_u256_mptr, + pcs: config.pcs, + } + } + + /// Validate fixed-window capacities and registered memory lifetimes. + pub(crate) fn validate(&self) -> Result<(), String> { + let fixed_low_memory = [ + ( + "constructor_smoke_scratch_mptr", + self.constructor_smoke_scratch_mptr, + LOW_MEMORY_SCRATCH_START, + ), + ( + "transcript_mptr", + self.transcript_mptr, + TRANSCRIPT_BUFFER_START, + ), + ( + "pcs_pairing_scratch_mptr", + self.pcs_pairing_scratch_mptr, + PCS_PAIRING_SCRATCH_START, + ), + ( + "verifier_return_mptr", + self.verifier_return_mptr, + VERIFIER_RETURN_BUFFER_START, + ), + ( + "quotient_return_mptr", + self.quotient_return_mptr, + QUOTIENT_RETURN_BUFFER_START, + ), + ( + "accumulator_pairing_batch_mptr", + self.accumulator_pairing_batch_mptr, + crate::lowering::layout::accumulator::PAIRING_BATCH_PTR, + ), + ( + "final_pairing_scratch_mptr", + self.final_pairing_scratch_mptr, + crate::lowering::layout::FINAL_PAIRING_SCRATCH_START, + ), + ]; + for (name, actual, expected) in fixed_low_memory { + if actual != expected { + return Err(format!( + "fixed memory pointer {name} drifted: got {actual:#x}, expected {expected:#x}" + )); + } + } + + let expected_scalar_inv = + self.vk_mptr.value().as_usize().saturating_sub(MODEXP_SCRATCH_BYTES); + if self.scalar_inv_scratch_mptr != expected_scalar_inv { + return Err(format!( + "scalar inversion scratch drifted: got {:#x}, expected {expected_scalar_inv:#x}", + self.scalar_inv_scratch_mptr + )); + } + + let windows = self.theta_windows; + if self.pcs.rot_points_words > windows.rot_points_cap_words { + return Err(format!( + "PCS scratch layout mismatch: ROT_POINTS_MPTR needs {} word(s), capacity is {}", + self.pcs.rot_points_words, windows.rot_points_cap_words + )); + } + if self.pcs.x1_powers_words > windows.x1_powers_cap_words { + return Err(format!( + "PCS scratch layout mismatch: X1_POWERS_MPTR needs {} word(s), capacity is {}", + self.pcs.x1_powers_words, windows.x1_powers_cap_words + )); + } + if self.pcs.q_com_words > windows.q_com_cap_words { + return Err(format!( + "PCS scratch layout mismatch: Q_COM_MPTR needs {} word(s), capacity is {}", + self.pcs.q_com_words, windows.q_com_cap_words + )); + } + if self.pcs.q_eval_set_words > windows.q_eval_set_cap_words { + return Err(format!( + "PCS scratch layout mismatch: Q_EVAL_SET_MPTR needs {} word(s), capacity is {}", + self.pcs.q_eval_set_words, windows.q_eval_set_cap_words + )); + } + + self.map.validate()?; + + Ok(()) + } +} + +/// Number of proof commitment G1 points materialized in verifier memory. +pub(crate) fn commitment_g1_count(meta: &ConstraintSystemMeta) -> usize { + meta.num_user_advices.iter().sum::() + + meta.num_lookups + + meta.num_permutation_zs + + meta.lookup_chunks.iter().sum::() + + meta.num_lookups + + meta.num_trashcans + + meta.num_quotients +} + +/// Scratch size required by the batched scalar-inversion helper. +fn batch_invert_scratch_bytes(meta: &ConstraintSystemMeta, num_instances: usize) -> usize { + // The template calls: + // batch_invert(X_N_MPTR, mptr_end + WORD_BYTES, scratch, r) + // + // The input range covers: + // - num_instances public Lagrange denominators, or one fallback word when + // there are no public instances; + // - `abs(rotation_last)` negative-row denominators; + // - x_n - 1. + // + // For N inputs, the batched inversion stores N-2 prefix products and then + // overlays one modexp frame at the current prefix pointer. Singletons use + // only the frame. + let input_words = if num_instances == 0 { + meta.rotation_last.unsigned_abs() as usize + 2 + } else { + num_instances + meta.rotation_last.unsigned_abs() as usize + 1 + }; + + MODEXP_FRAME_BYTES + input_words.saturating_sub(2) * WORD_BYTES +} + +#[cfg(test)] +mod tests { + use ruint::aliases::U256; + + use super::*; + use crate::lowering::render::G1Words; + + /// Test helper for phase-scoped memory-region fixtures. + fn region(name: &'static str, start: usize, len: usize, phase: MemoryPhase) -> MemoryRegion { + MemoryRegion::new(name, start, len, MemoryLifetime::Phase(phase)) + } + + #[test] + fn overlapping_permanent_regions_fail() { + let mut map = MemoryMap::default(); + map.push(MemoryRegion::new( + "a", + 0x100, + WORD_BYTES * 2, + MemoryLifetime::Permanent, + )); + map.push(MemoryRegion::new( + "b", + 0x120, + WORD_BYTES, + MemoryLifetime::Permanent, + )); + + let err = map.validate().unwrap_err(); + assert!(err.contains("overlaps")); + } + + #[test] + fn same_bytes_are_allowed_for_disjoint_phases() { + let mut map = MemoryMap::default(); + map.push(region("a", 0x100, WORD_BYTES, MemoryPhase::QuotientVm)); + map.push(region("b", 0x100, WORD_BYTES, MemoryPhase::PcsFinalMsm)); + + map.validate().expect("disjoint scratch lifetimes"); + } + + #[test] + fn unaligned_regions_fail() { + let mut map = MemoryMap::default(); + map.push(region("a", 0x101, WORD_BYTES, MemoryPhase::QuotientVm)); + assert!(map.validate().unwrap_err().contains("unaligned")); + + let mut map = MemoryMap::default(); + map.push(region("a", 0x100, WORD_BYTES - 1, MemoryPhase::QuotientVm)); + assert!(map.validate().unwrap_err().contains("unaligned")); + } + + #[test] + fn reserved_solidity_memory_regions_fail() { + let mut map = MemoryMap::default(); + map.push(region( + "free_memory_pointer", + crate::lowering::layout::SOLIDITY_FREE_MEMORY_POINTER_SLOT, + WORD_BYTES, + MemoryPhase::Transcript, + )); + let err = map.validate().unwrap_err(); + assert!( + err.contains("Solidity-reserved") && err.contains("free-memory pointer"), + "unexpected validation error: {err}" + ); + + let mut map = MemoryMap::default(); + map.push(region( + "zero_slot", + crate::lowering::layout::SOLIDITY_ZERO_SLOT, + WORD_BYTES, + MemoryPhase::Transcript, + )); + assert!(map.validate().unwrap_err().contains("Solidity-reserved")); + } + + #[test] + fn arena_alloc_after_aligns_after_anchor() { + let mut arena = MemoryArena::default(); + let after = arena.alloc_after( + "after", + 0x101, + WORD_BYTES, + WORD_BYTES, + MemoryLifetime::Permanent, + ); + let map = arena.into_map(); + + assert_eq!(after, 0x140); + assert_eq!(map.region("after").unwrap().start, 0x140); + map.validate().expect("allocated region is word-aligned"); + } + + #[test] + fn scratch_allocator_reuses_base_across_phases_and_advances_within_phase() { + let mut arena = MemoryArena::default(); + { + let mut scratch = arena.scratch_allocator(0x400); + let first = scratch.alloc_phase_scratch("first", WORD_BYTES, MemoryPhase::QuotientVm); + let second = scratch.alloc_phase_scratch("second", WORD_BYTES, MemoryPhase::QuotientVm); + let reused = + scratch.alloc_phase_scratch("reused", WORD_BYTES, MemoryPhase::PcsFinalMsm); + + assert_eq!(first, 0x400); + assert_eq!(second, 0x420); + assert_eq!(reused, 0x400); + } + + arena.into_map().validate().expect("scratch phases may reuse the same base"); + } + + #[test] + fn vk_constructor_payload_is_planner_registered() { + let layout = VkConstructorMemoryLayout::new(0x660); + let region = layout + .map + .region("vk_constructor_payload") + .expect("VK constructor payload registered"); + + assert_eq!(layout.payload_mptr, VK_CONSTRUCTOR_PAYLOAD_START); + assert_eq!(region.start, VK_CONSTRUCTOR_PAYLOAD_START); + assert_eq!(region.len, 0x660); + layout.validate().expect("VK constructor layout is valid"); + } + + #[test] + fn vk_constructor_runtime_region_rounds_up_unaligned_length() { + let layout = VkConstructorMemoryLayout::new(0x661); + let region = layout + .map + .region("vk_constructor_payload") + .expect("VK constructor payload registered"); + + assert_eq!(layout.payload_mptr, VK_CONSTRUCTOR_PAYLOAD_START); + assert_eq!(region.start, VK_CONSTRUCTOR_PAYLOAD_START); + assert_eq!(region.len, 0x680); + layout.validate().expect("unaligned runtime length reserves aligned memory"); + } + + /// Minimal VK model used by memory-layout regression tests. + fn synthetic_vk() -> Halo2VerifyingKey { + let fixed: Vec = vec![(U256::ZERO, U256::ZERO, U256::ZERO, U256::ZERO)]; + let constructor_memory = VkConstructorMemoryLayout::new( + crate::lowering::layout::VK_HEADER_WORDS * WORD_BYTES + fixed.len() * G1_BYTES, + ); + Halo2VerifyingKey { + constructor_payload_mptr: constructor_memory.payload_mptr, + constants: (0..crate::lowering::layout::VK_HEADER_WORDS) + .map(|_| ("c", U256::ZERO)) + .collect(), + fixed_comms: fixed, + permutation_comms: vec![], + quotient_const_offset_words: None, + quotient_const_words: 0, + quotient_program_offset_words: None, + quotient_program_words: 0, + } + } + + #[test] + fn synthetic_layout_preserves_current_offsets() { + let meta = ConstraintSystemMeta { + num_user_advices: vec![2], + num_lookups: 1, + num_permutation_zs: 1, + lookup_chunks: vec![2], + num_trashcans: 1, + num_quotients: 3, + num_evals: 5, + ..ConstraintSystemMeta::default() + }; + let vk = synthetic_vk(); + let layout = VerifierMemoryLayout::new( + &meta, + &vk, + Ptr::memory(0x1000), + VerifierMemoryLayoutConfig::default(), + ); + let theta = layout.theta_mptr.value().as_usize(); + let windows = layout.theta_windows; + + assert_eq!( + layout.rot_points_mptr.value().as_usize(), + theta + windows.rot_points_word * WORD_BYTES + ); + assert_eq!( + layout.x1_powers_mptr.value().as_usize(), + theta + windows.x1_powers_word * WORD_BYTES + ); + assert_eq!( + layout.q_eval_set_mptr.value().as_usize(), + theta + windows.q_eval_set_word * WORD_BYTES + ); + assert_eq!( + layout.q_eval_cptr_mptr.value().as_usize(), + theta + windows.q_eval_cptr_word * WORD_BYTES + ); + assert_eq!( + layout.g1_identity_mptr.value().as_usize(), + theta + windows.g1_identity_word * WORD_BYTES + ); + assert_eq!( + layout.reversed_evals_mptr.value().as_usize(), + theta + windows.reversed_evals_word * WORD_BYTES + ); + assert_eq!( + layout.comms_mptr_base.value().as_usize(), + theta + (windows.reversed_evals_word + meta.num_evals) * WORD_BYTES + ); + assert_eq!(windows.rot_points_word, theta_window::ROT_POINTS_WORD); + assert_eq!(windows.x1_powers_word, theta_window::X1_POWERS_WORD); + assert_eq!(windows.q_eval_set_word, theta_window::Q_EVAL_SET_WORD); + assert_eq!(windows.q_eval_cptr_word, theta_window::Q_EVAL_CPTR_WORD); + assert_eq!(windows.g1_identity_word, theta_window::G1_IDENTITY_WORD); + assert_eq!( + windows.reversed_evals_word, + theta_window::REVERSED_EVALS_WORD + ); + } + + #[test] + fn fixed_low_memory_regions_are_planner_registered() { + let meta = ConstraintSystemMeta { + num_simple_selectors: 3, + ..ConstraintSystemMeta::default() + }; + let vk = synthetic_vk(); + let layout = VerifierMemoryLayout::new( + &meta, + &vk, + Ptr::memory(0x1000), + VerifierMemoryLayoutConfig::default(), + ); + + let cases = [ + ( + "constructor_smoke_scratch", + layout.constructor_smoke_scratch_mptr, + LOW_MEMORY_SCRATCH_START, + PAIRING_PAIR_BYTES, + ), + ( + "transcript_buffer", + layout.transcript_mptr, + TRANSCRIPT_BUFFER_START, + 0, + ), + ( + "pcs_pairing_static_working", + layout.pcs_pairing_scratch_mptr, + PCS_PAIRING_SCRATCH_START, + PCS_STATIC_WORKING_WORDS * WORD_BYTES, + ), + ( + "verifier_return", + layout.verifier_return_mptr, + VERIFIER_RETURN_BUFFER_START, + WORD_BYTES, + ), + ( + "quotient_return", + layout.quotient_return_mptr, + QUOTIENT_RETURN_BUFFER_START, + (2 + meta.num_simple_selectors) * WORD_BYTES, + ), + ( + "accumulator_pairing_batch", + layout.accumulator_pairing_batch_mptr, + crate::lowering::layout::accumulator::PAIRING_BATCH_PTR, + ACCUMULATOR_PAIRING_BATCH_BYTES, + ), + ( + "final_pairing_scratch", + layout.final_pairing_scratch_mptr, + crate::lowering::layout::FINAL_PAIRING_SCRATCH_START, + PAIRING_STATIC_WORKING_WORDS * WORD_BYTES, + ), + ]; + + for (name, field, start, len) in cases { + let region = layout.map.region(name).expect("fixed region registered"); + assert_eq!(field, start, "{name} field drifted"); + assert_eq!(region.start, start, "{name} start drifted"); + assert_eq!(region.len, len, "{name} len drifted"); + } + + let scalar_inv = layout + .map + .region("scalar_inv_scratch") + .expect("scalar inversion scratch registered"); + assert_eq!( + layout.scalar_inv_scratch_mptr, + 0x1000 - MODEXP_SCRATCH_BYTES + ); + assert_eq!(scalar_inv.start, layout.scalar_inv_scratch_mptr); + assert_eq!(scalar_inv.len, MODEXP_FRAME_BYTES); + layout.validate().expect("fixed regions are phase-scoped"); + } + + #[test] + fn pcs_fixed_window_overflows_fail_with_clear_messages() { + let meta = ConstraintSystemMeta::default(); + let vk = synthetic_vk(); + let windows = ThetaWindowLayout::compatibility(); + let mut config = VerifierMemoryLayoutConfig::default(); + config.pcs.rot_points_words = windows.rot_points_cap_words + 1; + let layout = VerifierMemoryLayout::new(&meta, &vk, Ptr::memory(0x1000), config); + assert!(layout.validate().unwrap_err().contains("ROT_POINTS_MPTR")); + + let mut config = VerifierMemoryLayoutConfig::default(); + config.pcs.x1_powers_words = windows.x1_powers_cap_words + 1; + let layout = VerifierMemoryLayout::new(&meta, &vk, Ptr::memory(0x1000), config); + assert!(layout.validate().unwrap_err().contains("X1_POWERS_MPTR")); + + let mut config = VerifierMemoryLayoutConfig::default(); + config.pcs.q_eval_set_words = windows.q_eval_set_cap_words + 1; + let layout = VerifierMemoryLayout::new(&meta, &vk, Ptr::memory(0x1000), config); + assert!(layout.validate().unwrap_err().contains("Q_EVAL_SET_MPTR")); + } + + #[test] + fn accumulator_msm_region_is_sized_from_shape() { + let meta = ConstraintSystemMeta::default(); + let vk = synthetic_vk(); + let config = VerifierMemoryLayoutConfig { + acc_msm_terms: 4, + ..VerifierMemoryLayoutConfig::default() + }; + let layout = VerifierMemoryLayout::new(&meta, &vk, Ptr::memory(0x1000), config); + let region = + layout.map.region("accumulator_msm").expect("accumulator MSM region registered"); + + assert_eq!(region.len, 4 * G1_MSM_PAIR_BYTES); + } + + #[test] + fn trace_log_word_is_registered_after_scratch_regions() { + let meta = ConstraintSystemMeta { + num_user_advices: vec![8], + num_quotients: 4, + num_evals: 16, + ..ConstraintSystemMeta::default() + }; + let vk = synthetic_vk(); + let config = VerifierMemoryLayoutConfig { + acc_msm_terms: 3, + pcs: PcsMemoryRequirements { + q_com_trace_msm: FinalMsmShape::from_terms(9), + final_msm: FinalMsmShape::from_terms(10), + ..PcsMemoryRequirements::default() + }, + ..VerifierMemoryLayoutConfig::default() + }; + let layout = VerifierMemoryLayout::new(&meta, &vk, Ptr::memory(0x1000), config); + let region = + layout.map.region("trace_u256_log_word").expect("trace_u256 region registered"); + + assert_eq!(region.start, layout.trace_u256_mptr); + assert_eq!(region.len, WORD_BYTES); + assert!( + layout.trace_u256_mptr >= layout.pcs_final_msm_scratch_mptr + 10 * G1_MSM_PAIR_BYTES + ); + layout.validate().expect("trace log word must not overlap"); + } + + #[test] + fn batch_invert_scratch_region_tracks_instance_shape() { + let meta = ConstraintSystemMeta { + rotation_last: -3, + ..ConstraintSystemMeta::default() + }; + let vk = synthetic_vk(); + let config = VerifierMemoryLayoutConfig { + num_instances: 5, + ..VerifierMemoryLayoutConfig::default() + }; + let layout = VerifierMemoryLayout::new(&meta, &vk, Ptr::memory(0x1000), config); + let region = layout + .map + .region("batch_invert_scratch") + .expect("batch invert scratch region registered"); + + assert_eq!( + region.len, + MODEXP_FRAME_BYTES + (5 + 3 + 1 - 2) * WORD_BYTES + ); + } +} diff --git a/proofs/solidity-verifier/src/lowering/layout/mod.rs b/proofs/solidity-verifier/src/lowering/layout/mod.rs new file mode 100644 index 000000000..b9f7f24a7 --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/layout/mod.rs @@ -0,0 +1,771 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Shared codegen layout facts. +//! +//! This module is the single home for numeric facts that describe the +//! generated verifier's ABI, EVM word encodings, VK header, historical +//! theta-relative memory slots, and trace namespaces. Most of these values are +//! intentionally stable compatibility anchors; call sites should use the named +//! facts here instead of repeating raw literals. + +pub(crate) mod memory; +pub(crate) mod vk_payload; + +use ruint::aliases::U256; + +/// EVM word size. BLS12-381 Fr values are rendered as one canonical +/// big-endian EVM word in calldata/memory. +pub(crate) const WORD_BYTES: usize = 0x20; +/// Solidity's scratch space reserved for hashing and short-lived compiler use. +pub(crate) const SOLIDITY_SCRATCH_SPACE_BYTES: usize = 0x40; +/// Solidity free-memory pointer slot. +pub(crate) const SOLIDITY_FREE_MEMORY_POINTER_SLOT: usize = 0x40; +/// Solidity zero slot used as the initial value for dynamic memory arrays. +pub(crate) const SOLIDITY_ZERO_SLOT: usize = 0x60; +/// First byte not reserved by Solidity's memory conventions. +pub(crate) const SOLIDITY_ALLOCATABLE_MEMORY_START: usize = 0x80; +/// The full reserved prefix: scratch, free-memory pointer, and zero slot. +pub(crate) const SOLIDITY_RESERVED_MEMORY_BYTES: usize = SOLIDITY_ALLOCATABLE_MEMORY_START; +/// Generated verifier transcript and low-memory precompile scratch base. +pub(crate) const LOW_MEMORY_SCRATCH_START: usize = SOLIDITY_ALLOCATABLE_MEMORY_START; +/// Start of the Keccak transcript buffer used by the assembly helpers. +pub(crate) const TRANSCRIPT_BUFFER_START: usize = LOW_MEMORY_SCRATCH_START; +/// Shared low-memory scratch for PCS pairing serialization. +pub(crate) const PCS_PAIRING_SCRATCH_START: usize = LOW_MEMORY_SCRATCH_START; +/// Return buffer used by production verifier success paths. +pub(crate) const VERIFIER_RETURN_BUFFER_START: usize = LOW_MEMORY_SCRATCH_START; +/// Return buffer used by split quotient evaluator calls. +pub(crate) const QUOTIENT_RETURN_BUFFER_START: usize = LOW_MEMORY_SCRATCH_START; +/// Constructor-time memory base for the separate VK runtime payload. +pub(crate) const VK_CONSTRUCTOR_PAYLOAD_START: usize = LOW_MEMORY_SCRATCH_START; +/// Number of EVM words in one Fr scalar. +pub(crate) const FR_WORDS: usize = 1; +/// EIP-2537 padded G1 encoding: x_hi, x_lo, y_hi, y_lo. +pub(crate) const G1_WORDS: usize = 4; +/// EIP-2537 padded G2 encoding: four Fp2 coordinates, two words each. +pub(crate) const G2_WORDS: usize = 8; +/// Byte length of one padded Fr scalar. +pub(crate) const FR_BYTES: usize = FR_WORDS * WORD_BYTES; +/// Byte length of one padded G1 point. +pub(crate) const G1_BYTES: usize = G1_WORDS * WORD_BYTES; +/// Byte length of one padded G2 point. +pub(crate) const G2_BYTES: usize = G2_WORDS * WORD_BYTES; +/// Native midnight-proofs compressed G1 encoding before off-chain EIP-2537 +/// repack. +pub(crate) const G1_COMPRESSED_BYTES: usize = 48; +/// Big-endian byte length of an unpadded BLS12-381 base-field coordinate. +pub(crate) const BLS_FP_BYTES: usize = 48; +/// EIP-2537 pads each 48-byte Fp coordinate with 16 leading zero bytes. +pub(crate) const EIP2537_FP_PAD_BYTES: usize = 16; +/// EIP-2537 G1MSM input tuple: one padded G1 plus one scalar. +pub(crate) const G1_MSM_PAIR_BYTES: usize = G1_BYTES + FR_BYTES; +/// EIP-2537 G1ADD input tuple: two padded G1 points. +pub(crate) const G1ADD_INPUT_BYTES: usize = 2 * G1_BYTES; +/// EVM modexp frame for 32-byte base, exponent, and modulus. +/// +/// Layout: three 32-byte length words followed by base, exponent, modulus. +pub(crate) const MODEXP_FRAME_WORDS: usize = 6; +pub(crate) const MODEXP_FRAME_BYTES: usize = MODEXP_FRAME_WORDS * WORD_BYTES; +/// Historical low-memory reservation used by scalar inversion helpers near +/// `VK_MPTR`. It is larger than the frame because older code treated the +/// surrounding 0x100-byte window as scratch; keep the same distance from +/// `VK_MPTR` when checking overlaps. +pub(crate) const MODEXP_SCRATCH_WORDS: usize = 8; +pub(crate) const MODEXP_SCRATCH_BYTES: usize = MODEXP_SCRATCH_WORDS * WORD_BYTES; +/// EIP-2537 pairing precompile input for one `(G1, G2)` pair. +pub(crate) const PAIRING_PAIR_BYTES: usize = G1_BYTES + G2_BYTES; +/// Two-pair KZG pairing input: `(rhs, G2)` and `(lhs, -sG2)`. +pub(crate) const PAIRING_TWO_PAIR_BYTES: usize = 2 * PAIRING_PAIR_BYTES; +/// Historical floor for the accumulator MSM input buffer. +/// +/// This is deliberately not derived from the current proof shape: smaller +/// circuits keep the same generated addresses as the compatibility template. +pub(crate) const ACC_MSM_MIN_SCRATCH_BYTES: usize = 0x7000; +/// Static low-memory working set required by the PCS pairing helpers. +/// +/// The final pairing frame needs 25 words (two `(G1, G2)` pairs plus one return +/// word); production PCS code rounds this up to leave room for intermediate +/// in-Yul accumulators. +pub(crate) const PCS_STATIC_WORKING_WORDS: usize = 32; +/// Static two-pair KZG pairing scratch plus one return word. +pub(crate) const PAIRING_STATIC_WORKING_WORDS: usize = PAIRING_TWO_PAIR_BYTES / WORD_BYTES + 1; +/// Low-memory frame used by the final two-pair KZG pairing helper. +pub(crate) const FINAL_PAIRING_SCRATCH_START: usize = PAIRING_TWO_PAIR_BYTES; + +pub(crate) mod precompile { + //! EVM precompile addresses, frame lengths, and gas constants used by the + //! generated verifier. These values come from EIP-198 and EIP-2537; keep + //! template call sites wired through these names so future fork changes are + //! not hidden in hand-written Yul literals. + + /// EIP-198 modexp precompile. + pub(crate) const MODEXP_ADDRESS: usize = 0x05; + /// Prague/EIP-2537 BLS12-381 G1ADD precompile. + pub(crate) const G1ADD_ADDRESS: usize = 0x0b; + /// Prague/EIP-2537 BLS12-381 G1MSM precompile. + pub(crate) const G1MSM_ADDRESS: usize = 0x0c; + /// Prague/EIP-2537 BLS12-381 pairing precompile. + pub(crate) const PAIRING_ADDRESS: usize = 0x0f; + + /// Deployment smoke-test gas caps. They are intentionally above the single + /// operation costs, but still bounded enough to catch missing precompiles. + pub(crate) const G1ADD_GAS_CAP: usize = 50_000; + /// Gas cap for the deployment-time identity G1MSM smoke test. + pub(crate) const G1MSM_SMOKE_GAS_CAP: usize = 60_000; + /// Gas cap for the deployment-time identity pairing smoke test. + pub(crate) const PAIRING_SMOKE_GAS_CAP: usize = 120_000; + /// EIP-2537 G1MSM gas formula: `base + k * discount[k] * mul_cost / 1000`. + pub(crate) const G1MSM_BASE_GAS: usize = 50_000; + /// Per-scalar multiplication cost before applying the EIP-2537 discount. + pub(crate) const G1MSM_SCALAR_MULTIPLICATION_COST: usize = 12_000; + /// Denominator used by the EIP-2537 discount table. + pub(crate) const G1MSM_DISCOUNT_DENOMINATOR: usize = 1_000; + /// EIP-2537 pairing gas formula: base + pair_count * pair_cost. + pub(crate) const PAIRING_BASE_GAS: usize = 50_000; + pub(crate) const PAIRING_PAIR_GAS: usize = 60_000; + + /// EIP-2537 G1MSM discount table for k = 0..128. + /// + /// k=0 is not a real precompile input shape for verifier calls, but keeping + /// it in the table preserves the previous Yul helper's behavior. + pub(crate) const G1MSM_DISCOUNT_TABLE: [usize; 129] = [ + 0, 1000, 949, 848, 797, 764, 750, 738, 728, 719, 712, 705, 698, 692, 687, 682, 677, 673, + 669, 665, 661, 658, 654, 651, 648, 645, 642, 640, 637, 635, 632, 630, 627, 625, 623, 621, + 619, 617, 615, 613, 611, 609, 608, 606, 604, 603, 601, 599, 598, 596, 595, 593, 592, 591, + 589, 588, 586, 585, 584, 582, 581, 580, 579, 577, 576, 575, 574, 573, 572, 570, 569, 568, + 567, 566, 565, 564, 563, 562, 561, 560, 559, 558, 557, 556, 555, 554, 553, 552, 551, 550, + 549, 548, 547, 547, 546, 545, 544, 543, 542, 541, 540, 540, 539, 538, 537, 536, 536, 535, + 534, 533, 532, 532, 531, 530, 529, 528, 528, 527, 526, 525, 525, 524, 523, 522, 522, 521, + 520, 520, 519, + ]; + + /// Return the bounded gas cap for a G1MSM input of `input_len` bytes. + /// + /// Pinned verifier codegen knows each static MSM input length, so rendering + /// this value as a literal avoids carrying the whole discount-table switch + /// in deployed Yul. + pub(crate) fn g1msm_gas_cap(input_len: usize) -> usize { + let k = input_len / super::G1_MSM_PAIR_BYTES; + let discount = G1MSM_DISCOUNT_TABLE.get(k).copied().unwrap_or(519); + G1MSM_BASE_GAS + + k * discount * G1MSM_SCALAR_MULTIPLICATION_COST / G1MSM_DISCOUNT_DENOMINATOR + } + + /// Return the bounded gas cap for a pairing input of `input_len` bytes. + pub(crate) fn pairing_gas_cap(input_len: usize) -> usize { + PAIRING_BASE_GAS + (input_len / super::PAIRING_PAIR_BYTES) * PAIRING_PAIR_GAS + } +} + +pub(crate) mod modexp_frame { + //! 32-byte base/exponent/modulus EIP-198 frame offsets. + + use super::WORD_BYTES; + + pub(crate) const BASE_LEN_OFFSET: usize = 0; + /// Exponent-length word offset. + pub(crate) const EXP_LEN_OFFSET: usize = WORD_BYTES; + /// Modulus-length word offset. + pub(crate) const MOD_LEN_OFFSET: usize = 2 * WORD_BYTES; + /// Base value word offset. + pub(crate) const BASE_OFFSET: usize = 3 * WORD_BYTES; + /// Exponent value word offset. + pub(crate) const EXP_OFFSET: usize = 4 * WORD_BYTES; + /// Modulus value word offset. + pub(crate) const MOD_OFFSET: usize = 5 * WORD_BYTES; +} + +pub(crate) mod accumulator { + //! Public-input encoding used by `AssignedAccumulator::as_public_input` + //! for the current BLS12-381 self-emulation circuits. + + use super::{G1_BYTES, WORD_BYTES}; + + /// `AssignedField::as_public_input` uses seven radix-2^56 limbs for one + /// BLS12-381 base-field coordinate. + pub(crate) const LIMB_BITS: usize = 56; + pub(crate) const LIMBS: usize = 7; + /// Four 56-bit limbs fit in one scalar word with unused high bits. + pub(crate) const LIMBS_PER_WORD: usize = 4; + /// Carried scalars in the point-and-scalar accumulator public input. + pub(crate) const CARRIED_SCALARS: usize = 2; + /// Low-memory hash frame for batching the accumulator pairing with KZG: + /// domain tag word, KZG rhs/lhs G1s, then accumulator rhs/lhs G1s. + pub(crate) const PAIRING_BATCH_PTR: usize = 0x100; + /// ASCII `"pairing-batch-acc-kzg"` right-padded to one EVM word. + pub(crate) const PAIRING_BATCH_DOMAIN_TAG_HEX: &str = + "0x70616972696e672d62617463682d6163632d6b7a670000000000000000"; + /// KZG pairing RHS point offset inside the batch hash frame. + pub(crate) const PAIRING_BATCH_RHS_OFFSET: usize = WORD_BYTES; + /// KZG pairing LHS point offset inside the batch hash frame. + pub(crate) const PAIRING_BATCH_LHS_OFFSET: usize = WORD_BYTES + G1_BYTES; + /// Accumulator RHS point offset inside the batch hash frame. + pub(crate) const PAIRING_BATCH_ACC_RHS_OFFSET: usize = WORD_BYTES + 2 * G1_BYTES; + /// Accumulator LHS point offset inside the batch hash frame. + pub(crate) const PAIRING_BATCH_ACC_LHS_OFFSET: usize = WORD_BYTES + 3 * G1_BYTES; + /// Number of frame bytes absorbed into the accumulator/KZG batch challenge. + pub(crate) const PAIRING_BATCH_HASH_BYTES: usize = WORD_BYTES + 4 * G1_BYTES; +} + +pub(crate) mod quotient_limb { + //! Foreign-field limb-specialized quotient VM shapes. + + /// Matches the accumulator/public-input BLS12-381 base-field limb packing. + pub(crate) const LIMBS: usize = 7; + /// Linear reduction coefficients for limbs 1..6 after keeping limb 0 raw. + pub(crate) const LIN_COEFFS: usize = LIMBS - 1; + /// Full 7x7 schoolbook multiplication grid. + pub(crate) const PAIRWISE_TERMS: usize = LIMBS * LIMBS; + /// Output diagonals for a 7-limb by 7-limb product. + pub(crate) const PAIRWISE_COEFFS: usize = 2 * LIMBS - 1; +} + +pub(crate) mod transcript { + //! Transcript buffer bound heuristics. These are not protocol constants; + //! they are gas/codegen guardrails used by `transcript_buffer_words_bound`. + + use super::{G1_BYTES, WORD_BYTES}; + + /// Bytes written by one scalar transcript absorb. + pub(crate) const WORD_ABSORB_BYTES: usize = WORD_BYTES; + /// Bytes written by one uncompressed G1 transcript absorb. + pub(crate) const G1_ABSORB_BYTES: usize = G1_BYTES; + /// Cushion for the post-squeeze digest seed kept in the buffer. + pub(crate) const POST_SQUEEZE_CUSHION_WORDS: usize = 32; +} + +pub(crate) mod abi { + /// Solidity selector length before ABI-encoded arguments. + pub(crate) const SELECTOR_BYTES: usize = 0x04; + /// `verifyProof(bytes,uint256[])` has two ABI head words after the + /// selector. + pub(crate) const VERIFY_PROOF_HEAD_WORDS: usize = 2; + /// `verifyProof(bytes,uint256[])` ABI head size after the selector. + pub(crate) const VERIFY_PROOF_HEAD_BYTES: usize = VERIFY_PROOF_HEAD_WORDS * super::WORD_BYTES; + /// Calldata byte offset where the dynamic `proof` byte payload starts: + /// selector (0x04) + two ABI head words (0x40) + proof length word (0x20). + pub(crate) const VERIFY_PROOF_PROOF_CPTR: usize = + SELECTOR_BYTES + VERIFY_PROOF_HEAD_BYTES + super::WORD_BYTES; + /// Expected first ABI head word: offset to `proof`. + pub(crate) const VERIFY_PROOF_PROOF_HEAD_OFFSET: usize = VERIFY_PROOF_HEAD_BYTES; +} + +/// Number of scalar words before embedded SRS base points in the VK header. +const VK_HEADER_SCALAR_WORDS: usize = 11; +/// Header word where the padded G1 generator starts. +const VK_HEADER_G1_BASE_WORD: usize = VK_HEADER_SCALAR_WORDS; +/// Header word where the padded G2 generator starts. +const VK_HEADER_G2_BASE_WORD: usize = VK_HEADER_G1_BASE_WORD + G1_WORDS; +/// Header word where the padded `-sG2` point starts. +const VK_HEADER_NEG_S_G2_BASE_WORD: usize = VK_HEADER_G2_BASE_WORD + G2_WORDS; + +/// Stable word slots in the generated verifying-key header. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(usize)] +pub(crate) enum VkHeaderSlot { + /// Fiat-Shamir VK digest absorbed before proof data. + VkDigest = 0, + /// Expected public instance count. + NumInstances = 1, + /// Evaluation-domain exponent. + K = 2, + /// Multiplicative inverse of domain size. + NInv = 3, + /// Domain generator. + Omega = 4, + /// Inverse domain generator. + OmegaInv = 5, + /// `omega_inv^last_rotation_abs` for negative-row Lagrange terms. + OmegaInvToL = 6, + /// Boolean flag for optional public accumulator batching. + HasAccumulator = 7, + /// Public-input offset of accumulator encoding. + AccOffset = 8, + /// Limbs per BLS base-field coordinate in accumulator encoding. + NumAccLimbs = 9, + /// Bits per accumulator limb. + NumAccLimbBits = 10, + /// Padded G1 generator. + G1Base = VK_HEADER_G1_BASE_WORD, + /// Padded G2 generator. + G2Base = VK_HEADER_G2_BASE_WORD, + /// Padded negated SRS `sG2` point. + NegSG2Base = VK_HEADER_NEG_S_G2_BASE_WORD, +} + +impl VkHeaderSlot { + /// Return the word offset of this header slot. + pub(crate) const fn word(self) -> usize { + self as usize + } +} + +/// Total number of EVM words in the VK header. +pub(crate) const VK_HEADER_WORDS: usize = VK_HEADER_NEG_S_G2_BASE_WORD + G2_WORDS; + +/// Schema descriptor for one VK header field. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct VkHeaderFieldSpec { + /// First slot occupied by the field. + pub(crate) slot: VkHeaderSlot, + /// Stable source/debug name. + pub(crate) name: &'static str, + /// Field width in EVM words. + pub(crate) width_words: usize, +} + +/// Ordered VK header schema. +pub(crate) const VK_HEADER_FIELDS: [VkHeaderFieldSpec; 14] = [ + VkHeaderFieldSpec { + slot: VkHeaderSlot::VkDigest, + name: "vk_digest", + width_words: 1, + }, + VkHeaderFieldSpec { + slot: VkHeaderSlot::NumInstances, + name: "num_instances", + width_words: 1, + }, + VkHeaderFieldSpec { + slot: VkHeaderSlot::K, + name: "k", + width_words: 1, + }, + VkHeaderFieldSpec { + slot: VkHeaderSlot::NInv, + name: "n_inv", + width_words: 1, + }, + VkHeaderFieldSpec { + slot: VkHeaderSlot::Omega, + name: "omega", + width_words: 1, + }, + VkHeaderFieldSpec { + slot: VkHeaderSlot::OmegaInv, + name: "omega_inv", + width_words: 1, + }, + VkHeaderFieldSpec { + slot: VkHeaderSlot::OmegaInvToL, + name: "omega_inv_to_l", + width_words: 1, + }, + VkHeaderFieldSpec { + slot: VkHeaderSlot::HasAccumulator, + name: "has_accumulator", + width_words: 1, + }, + VkHeaderFieldSpec { + slot: VkHeaderSlot::AccOffset, + name: "acc_offset", + width_words: 1, + }, + VkHeaderFieldSpec { + slot: VkHeaderSlot::NumAccLimbs, + name: "num_acc_limbs", + width_words: 1, + }, + VkHeaderFieldSpec { + slot: VkHeaderSlot::NumAccLimbBits, + name: "num_acc_limb_bits", + width_words: 1, + }, + VkHeaderFieldSpec { + slot: VkHeaderSlot::G1Base, + name: "G1_BASE", + width_words: G1_WORDS, + }, + VkHeaderFieldSpec { + slot: VkHeaderSlot::G2Base, + name: "G2_BASE", + width_words: G2_WORDS, + }, + VkHeaderFieldSpec { + slot: VkHeaderSlot::NegSG2Base, + name: "NEG_S_G2_BASE", + width_words: G2_WORDS, + }, +]; + +/// Accessor namespace for the stable VK header schema. +pub(crate) struct VkHeaderLayout; + +impl VkHeaderLayout { + /// Return the total header width in EVM words. + pub(crate) const fn header_words() -> usize { + VK_HEADER_WORDS + } + + /// Return every header field descriptor in render order. + pub(crate) fn fields() -> &'static [VkHeaderFieldSpec] { + &VK_HEADER_FIELDS + } + + /// Return the descriptor for a single slot. + pub(crate) fn field(slot: VkHeaderSlot) -> VkHeaderFieldSpec { + let fields = Self::fields(); + let mut idx = 0; + while idx < fields.len() { + if fields[idx].slot as usize == slot as usize { + return fields[idx]; + } + idx += 1; + } + panic!("unknown VK header slot") + } + + /// Create an empty checked header builder. + pub(crate) fn builder() -> VkHeaderBuilder { + VkHeaderBuilder::new() + } +} + +/// Checked writer for the VK header constant vector. +#[derive(Clone, Debug)] +pub(crate) struct VkHeaderBuilder { + words: Vec>, +} + +impl VkHeaderBuilder { + /// Create a builder with every header word initially unset. + pub(crate) fn new() -> Self { + Self { + words: vec![None; VkHeaderLayout::header_words()], + } + } + + /// Insert a field at its schema-defined slot, checking width and overlap. + pub(crate) fn insert( + &mut self, + slot: VkHeaderSlot, + expected_width: usize, + values: &[(&'static str, U256)], + ) -> Result<(), String> { + let spec = VkHeaderLayout::field(slot); + if spec.width_words != expected_width { + return Err(format!( + "VK header field {} width mismatch: descriptor={} caller={expected_width}", + spec.name, spec.width_words + )); + } + if values.len() != spec.width_words { + return Err(format!( + "VK header field {} expected {} word(s), got {}", + spec.name, + spec.width_words, + values.len() + )); + } + let start = spec.slot.word(); + let end = start + spec.width_words; + if end > self.words.len() { + return Err(format!( + "VK header field {} overflows header: range {start}..{end}, header={}", + spec.name, + self.words.len() + )); + } + for (offset, value) in values.iter().copied().enumerate() { + let word = start + offset; + if self.words[word].is_some() { + return Err(format!( + "VK header field {} overlaps already-written word {word}", + spec.name + )); + } + self.words[word] = Some(value); + } + Ok(()) + } + + /// Insert a one-word scalar header field. + pub(crate) fn scalar( + &mut self, + slot: VkHeaderSlot, + name: &'static str, + value: U256, + ) -> Result<(), String> { + self.insert(slot, 1, &[(name, value)]) + } + + /// Insert a padded G1 header field. + pub(crate) fn g1( + &mut self, + slot: VkHeaderSlot, + names: [&'static str; G1_WORDS], + values: [U256; G1_WORDS], + ) -> Result<(), String> { + let words = names.into_iter().zip(values).collect::>(); + self.insert(slot, G1_WORDS, &words) + } + + /// Insert a padded G2 header field. + pub(crate) fn g2( + &mut self, + slot: VkHeaderSlot, + names: [&'static str; G2_WORDS], + values: [U256; G2_WORDS], + ) -> Result<(), String> { + let words = names.into_iter().zip(values).collect::>(); + self.insert(slot, G2_WORDS, &words) + } + + /// Return the completed header or report the first missing word. + pub(crate) fn finish(self) -> Result, String> { + self.words + .into_iter() + .enumerate() + .map(|(word, value)| { + value.ok_or_else(|| format!("VK header word {word} was not written")) + }) + .collect() + } +} + +/// Historical word slots rooted at `THETA_MPTR`. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(usize)] +pub(crate) enum ThetaSlot { + /// Lookup compression challenge. + Theta = 0, + /// Permutation/lookup beta challenge. + Beta = 1, + /// Permutation/lookup gamma challenge. + Gamma = 2, + /// Trash argument compression challenge. + TrashChallenge = 3, + /// Quotient identity batch challenge. + Y = 4, + /// Evaluation point. + X = 5, + /// PCS challenge x1. + X1 = 6, + /// PCS challenge x2. + X2 = 7, + /// PCS challenge x3. + X3 = 8, + /// PCS challenge x4. + X4 = 9, + /// KZG `f_com` G1 slot. + FCom = 10, + /// KZG proof commitment `pi` G1 slot. + Pi = 14, + /// Public accumulator left-hand G1 slot. + AccLhs = 18, + /// Public accumulator right-hand G1 slot. + AccRhs = 22, + /// `x^n` scalar slot. + XN = 26, + /// `(x^n - 1)^-1` scalar slot. + XNMinus1Inv = 27, + /// Last-row Lagrange scalar slot. + LLast = 28, + /// Blinding-row Lagrange scalar slot. + LBlind = 29, + /// First-row Lagrange scalar slot. + L0 = 30, + /// Locally interpolated public-instance evaluation. + InstanceEval = 31, + /// Negated quotient numerator evaluation. + QuotientEval = 32, + /// Quotient linearization scratch G1-sized slot. + Quotient = 33, + /// PCS `f_eval` scalar slot. + FEval = 38, + /// PCS `v` scalar slot. + V = 39, + /// Final linearized commitment G1 slot. + FinalCom = 40, + /// Final pairing LHS G1 slot. + PairingLhs = 44, + /// Final pairing RHS G1 slot. + PairingRhs = 48, +} + +impl ThetaSlot { + /// Return this slot's word offset relative to `THETA_MPTR`. + pub(crate) const fn word(self) -> usize { + self as usize + } +} + +pub(crate) mod theta_window { + //! Historical word offsets from `THETA_MPTR` for the fixed PCS windows. + //! + //! The first 52 words are covered by `ThetaSlot`: ten challenge/scalar + //! words, four G1 slots, more scalar state, a historical padding word, + //! and three final G1 slots. The windows below preserve the old + //! generated layout while making their capacities explicit. + + use super::{ThetaSlot, G1_WORDS}; + + /// Maximum serialized rotation points kept in the theta-relative window. + pub(crate) const ROT_POINTS_CAP_WORDS: usize = 28; + /// Maximum x1 powers kept in the theta-relative window. + pub(crate) const X1_POWERS_CAP_WORDS: usize = 65; + /// Maximum q_eval source pointers in one point-set window. + pub(crate) const Q_EVAL_SET_CAP_WORDS: usize = 56; + /// Historical padding between q_eval cursor and G1 identity. + pub(crate) const Q_EVAL_CPTR_PADDING_WORDS: usize = 7; + /// Historical padding after the G1 identity slot. + pub(crate) const G1_IDENTITY_PADDING_WORDS: usize = 7; + + /// Word offset of the serialized rotation-point window. + pub(crate) const ROT_POINTS_WORD: usize = ThetaSlot::PairingRhs.word() + G1_WORDS; + /// Word offset of the x1-powers window. + pub(crate) const X1_POWERS_WORD: usize = ROT_POINTS_WORD + ROT_POINTS_CAP_WORDS; + /// Word offset where quotient-commitment scratch starts. + pub(crate) const Q_COM_WORD: usize = X1_POWERS_WORD + X1_POWERS_CAP_WORDS; + /// Word offset where q_eval set scratch aliases q_com scratch. + pub(crate) const Q_EVAL_SET_WORD: usize = Q_COM_WORD; + /// Historical q_com capacity; currently zero because the window aliases. + pub(crate) const Q_COM_CAP_WORDS: usize = Q_EVAL_SET_WORD - Q_COM_WORD; + /// Word offset of the q_eval calldata cursor. + pub(crate) const Q_EVAL_CPTR_WORD: usize = Q_EVAL_SET_WORD + Q_EVAL_SET_CAP_WORDS; + /// Word offset of the canonical G1 identity slot. + pub(crate) const G1_IDENTITY_WORD: usize = Q_EVAL_CPTR_WORD + 1 + Q_EVAL_CPTR_PADDING_WORDS; + /// Word offset where reversed proof evaluations begin. + pub(crate) const REVERSED_EVALS_WORD: usize = + G1_IDENTITY_WORD + G1_WORDS + G1_IDENTITY_PADDING_WORDS; +} + +pub(crate) mod trace { + //! LOG topic namespaces used by trace-enabled builds. + //! + //! The gaps are intentional: they keep broad event families visually + //! separated in traces and preserve historical IDs consumed by tests and + //! external comparison scripts. + + pub(crate) const PCS_QUERY_BASE: u64 = 2_000; // KZG query trace IDs. + pub(crate) const PROOF_COMMIT_BASE: usize = 10_000; // Proof G1 reads. + pub(crate) const PROOF_EVAL_BASE: usize = 20_000; // Proof scalar eval reads. + pub(crate) const QUOTIENT_IDENTITY_BASE: u64 = 30_000; // Quotient identities. + pub(crate) const PCS_SERIALIZED_POINT_SET_BASE: u64 = 41_000; // PCS point sets. + pub(crate) const SELECTOR_FOLD_BASE: usize = 60_000; // Selector accumulators. +} + +#[cfg(test)] +mod tests { + use ruint::aliases::U256; + + use super::{ + abi, accumulator, modexp_frame, precompile, quotient_limb, theta_window, trace, transcript, + ThetaSlot, VkHeaderLayout, VkHeaderSlot, + }; + + #[test] + fn abi_offsets_match_verify_proof_calldata_layout() { + assert_eq!(abi::SELECTOR_BYTES, 0x04); + assert_eq!(abi::VERIFY_PROOF_HEAD_BYTES, 0x40); + assert_eq!(abi::VERIFY_PROOF_PROOF_CPTR, 0x64); + assert_eq!(abi::VERIFY_PROOF_PROOF_HEAD_OFFSET, 0x40); + } + + #[test] + fn solidity_reserved_memory_constants_match_compiler_conventions() { + assert_eq!(super::SOLIDITY_SCRATCH_SPACE_BYTES, 0x40); + assert_eq!(super::SOLIDITY_FREE_MEMORY_POINTER_SLOT, 0x40); + assert_eq!(super::SOLIDITY_ZERO_SLOT, 0x60); + assert_eq!(super::SOLIDITY_RESERVED_MEMORY_BYTES, 0x80); + assert_eq!(super::TRANSCRIPT_BUFFER_START, 0x80); + assert_eq!(super::VK_CONSTRUCTOR_PAYLOAD_START, 0x80); + } + + #[test] + fn vk_header_slots_preserve_generated_payload_layout() { + assert_eq!(VkHeaderSlot::VkDigest.word(), 0); + assert_eq!(VkHeaderSlot::G1Base.word(), 11); + assert_eq!(VkHeaderSlot::G2Base.word(), 15); + assert_eq!(VkHeaderSlot::NegSG2Base.word(), 23); + assert_eq!(VkHeaderLayout::header_words(), 31); + assert_eq!( + VkHeaderLayout::fields().iter().map(|field| field.width_words).sum::(), + 31 + ); + } + + #[test] + fn vk_header_builder_rejects_missing_duplicate_and_width_errors() { + let builder = VkHeaderLayout::builder(); + assert!(builder.finish().unwrap_err().contains("word 0")); + + let mut builder = VkHeaderLayout::builder(); + builder.scalar(VkHeaderSlot::VkDigest, "vk_digest", U256::from(1u64)).unwrap(); + let err = builder + .scalar(VkHeaderSlot::VkDigest, "vk_digest", U256::from(2u64)) + .unwrap_err(); + assert!(err.contains("overlaps")); + + let mut builder = VkHeaderLayout::builder(); + let err = builder + .insert(VkHeaderSlot::G1Base, 8, &[("too_wide", U256::ZERO)]) + .unwrap_err(); + assert!(err.contains("width mismatch")); + + let mut builder = super::VkHeaderBuilder { + words: vec![None; super::VK_HEADER_WORDS - 1], + }; + let words = [("neg_s_g2", U256::ZERO); super::G2_WORDS]; + let err = builder.insert(VkHeaderSlot::NegSG2Base, super::G2_WORDS, &words).unwrap_err(); + assert!(err.contains("overflows")); + } + + #[test] + fn theta_slots_and_windows_preserve_historical_offsets() { + assert_eq!(ThetaSlot::Theta.word(), 0); + assert_eq!(ThetaSlot::Quotient.word(), 33); + assert_eq!(ThetaSlot::PairingRhs.word(), 48); + assert_eq!(theta_window::ROT_POINTS_WORD, 52); + assert_eq!(theta_window::X1_POWERS_WORD, 80); + assert_eq!(theta_window::Q_EVAL_SET_WORD, 145); + assert_eq!(theta_window::Q_EVAL_CPTR_WORD, 201); + assert_eq!(theta_window::G1_IDENTITY_WORD, 209); + assert_eq!(theta_window::REVERSED_EVALS_WORD, 220); + } + + #[test] + fn trace_namespaces_preserve_existing_ids() { + assert_eq!(trace::PCS_QUERY_BASE, 2_000); + assert_eq!(trace::PROOF_COMMIT_BASE, 10_000); + assert_eq!(trace::PROOF_EVAL_BASE, 20_000); + assert_eq!(trace::QUOTIENT_IDENTITY_BASE, 30_000); + assert_eq!(trace::PCS_SERIALIZED_POINT_SET_BASE, 41_000); + assert_eq!(trace::SELECTOR_FOLD_BASE, 60_000); + } + + #[test] + fn precompile_and_modexp_constants_preserve_existing_frames() { + assert_eq!(precompile::MODEXP_ADDRESS, 0x05); + assert_eq!(precompile::G1ADD_ADDRESS, 0x0b); + assert_eq!(precompile::G1MSM_ADDRESS, 0x0c); + assert_eq!(precompile::PAIRING_ADDRESS, 0x0f); + assert_eq!(precompile::G1ADD_GAS_CAP, 50_000); + assert_eq!(precompile::G1MSM_SMOKE_GAS_CAP, 60_000); + assert_eq!(precompile::PAIRING_SMOKE_GAS_CAP, 120_000); + assert_eq!(modexp_frame::BASE_LEN_OFFSET, 0x00); + assert_eq!(modexp_frame::EXP_LEN_OFFSET, 0x20); + assert_eq!(modexp_frame::MOD_LEN_OFFSET, 0x40); + assert_eq!(modexp_frame::BASE_OFFSET, 0x60); + assert_eq!(modexp_frame::EXP_OFFSET, 0x80); + assert_eq!(modexp_frame::MOD_OFFSET, 0xa0); + } + + #[test] + fn accumulator_and_limb_constants_preserve_current_encoding() { + assert_eq!(accumulator::LIMB_BITS, 56); + assert_eq!(accumulator::LIMBS, 7); + assert_eq!(accumulator::LIMBS_PER_WORD, 4); + assert_eq!(accumulator::PAIRING_BATCH_PTR, 0x100); + assert_eq!(accumulator::PAIRING_BATCH_HASH_BYTES, 0x220); + assert_eq!(quotient_limb::LIMBS, 7); + assert_eq!(quotient_limb::PAIRWISE_TERMS, 49); + assert_eq!(quotient_limb::PAIRWISE_COEFFS, 13); + } + + #[test] + fn transcript_bound_constants_preserve_absorb_sizes() { + assert_eq!(transcript::WORD_ABSORB_BYTES, 0x20); + assert_eq!(transcript::G1_ABSORB_BYTES, 0x80); + assert_eq!(transcript::POST_SQUEEZE_CUSHION_WORDS, 32); + } +} diff --git a/proofs/solidity-verifier/src/lowering/layout/vk_payload.rs b/proofs/solidity-verifier/src/lowering/layout/vk_payload.rs new file mode 100644 index 000000000..c7c21f047 --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/layout/vk_payload.rs @@ -0,0 +1,322 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Typed payload layout for generated verifier artifacts. +//! +//! This is the small, local piece we borrow from the reusable-artifact style: +//! generated byte payloads should have one validated section map instead of +//! scattered offset arithmetic. The Solidity shape remains static and pinned. + +use ruint::aliases::U256; + +use crate::lowering::layout::{G1_WORDS, WORD_BYTES}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum PayloadSectionKind { + /// Fixed VK header words consumed by both embedded and external VK paths. + Header, + /// Deduplicated Fr constants used by the compact quotient VM. + QuotientConstants, + /// Packed compact-VM bytecode words. + QuotientProgram, + /// EIP-2537-padded fixed polynomial commitments. + FixedCommitments, + /// EIP-2537-padded permutation commitment bases. + PermutationCommitments, +} + +/// One contiguous word-aligned section inside the generated VK payload. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct PayloadSection { + /// Logical section kind. + pub(crate) kind: PayloadSectionKind, + /// Start word relative to the beginning of the VK payload. + pub(crate) word_offset: usize, + /// Length in EVM words. + pub(crate) word_len: usize, +} + +impl PayloadSection { + /// End word offset, exclusive. + pub(crate) fn word_end(self) -> usize { + self.word_offset + self.word_len + } + + /// Start byte offset relative to the beginning of the VK payload. + #[cfg(test)] + pub(crate) fn byte_offset(self) -> usize { + self.word_offset * WORD_BYTES + } + + /// Length in bytes. + #[cfg(test)] + pub(crate) fn byte_len(self) -> usize { + self.word_len * WORD_BYTES + } +} + +/// Monotonic section map for the generated VK byte payload. +/// +/// The layout is append-only by construction so template emission, runtime +/// `extcodecopy`, and tests all agree on the same section boundaries. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub(crate) struct VkPayloadLayout { + sections: Vec, + cursor_words: usize, +} + +impl VkPayloadLayout { + /// Start an empty layout with the cursor at word zero. + pub(crate) fn new() -> Self { + Self::default() + } + + /// Build the complete static VK payload section map. + /// + /// Commitment counts are converted into EIP-2537 G1 word widths here so + /// callers do not repeat that arithmetic at the emission sites. + pub(crate) fn for_vk( + header_words: usize, + quotient_const_words: usize, + quotient_program_words: usize, + fixed_commitments: usize, + permutation_commitments: usize, + ) -> Result { + let mut layout = Self::new(); + layout.reserve(PayloadSectionKind::Header, header_words)?; + layout.reserve(PayloadSectionKind::QuotientConstants, quotient_const_words)?; + layout.reserve(PayloadSectionKind::QuotientProgram, quotient_program_words)?; + layout.reserve_g1(PayloadSectionKind::FixedCommitments, fixed_commitments)?; + layout.reserve_g1( + PayloadSectionKind::PermutationCommitments, + permutation_commitments, + )?; + layout.validate()?; + Ok(layout) + } + + /// Reserve a generic word section at the current cursor. + /// + /// Each logical kind may appear at most once; duplicate sections would make + /// generated offsets ambiguous and are rejected immediately. + pub(crate) fn reserve( + &mut self, + kind: PayloadSectionKind, + word_len: usize, + ) -> Result { + if self.sections.iter().any(|section| section.kind == kind) { + return Err(format!("duplicate VK payload section: {kind:?}")); + } + + let section = PayloadSection { + kind, + word_offset: self.cursor_words, + word_len, + }; + self.cursor_words += word_len; + self.sections.push(section); + Ok(section) + } + + /// Reserve a section containing `commitments` EIP-2537-padded G1 points. + pub(crate) fn reserve_g1( + &mut self, + kind: PayloadSectionKind, + commitments: usize, + ) -> Result { + self.reserve(kind, commitments * G1_WORDS) + } + + /// Return the section for `kind`, if it has been reserved. + pub(crate) fn section(&self, kind: PayloadSectionKind) -> Option { + self.sections.iter().copied().find(|section| section.kind == kind) + } + + /// Return the section for `kind` or an explanatory layout error. + pub(crate) fn require_section( + &self, + kind: PayloadSectionKind, + ) -> Result { + self.section(kind) + .ok_or_else(|| format!("missing VK payload section: {kind:?}")) + } + + /// Start word of a required section. + pub(crate) fn word_offset(&self, kind: PayloadSectionKind) -> Result { + Ok(self.require_section(kind)?.word_offset) + } + + /// Word length of a required section. + pub(crate) fn word_len(&self, kind: PayloadSectionKind) -> Result { + Ok(self.require_section(kind)?.word_len) + } + + /// Start byte offset of a required section. + #[cfg(test)] + pub(crate) fn byte_offset(&self, kind: PayloadSectionKind) -> Result { + Ok(self.require_section(kind)?.byte_offset()) + } + + /// Byte length of a required section. + #[cfg(test)] + pub(crate) fn byte_len(&self, kind: PayloadSectionKind) -> Result { + Ok(self.require_section(kind)?.byte_len()) + } + + /// Total payload length in words. + pub(crate) fn total_words(&self) -> usize { + self.cursor_words + } + + /// Total payload length in bytes. + pub(crate) fn total_bytes(&self) -> usize { + self.total_words() * WORD_BYTES + } + + /// Check that sections are contiguous and the cursor matches their end. + pub(crate) fn validate(&self) -> Result<(), String> { + let mut cursor = 0usize; + for section in &self.sections { + if section.word_offset != cursor { + return Err(format!( + "VK payload section {:?} starts at {:#x}, expected {cursor:#x}", + section.kind, section.word_offset + )); + } + cursor = section.word_end(); + } + if cursor != self.cursor_words { + return Err(format!( + "VK payload cursor mismatch: sections end at {cursor:#x}, cursor is {:#x}", + self.cursor_words + )); + } + Ok(()) + } +} + +/// Codec for storing byte-oriented quotient programs in VK constant words. +pub(crate) struct PackedProgramCodec; + +impl PackedProgramCodec { + /// Number of EVM words required to hold `byte_len` bytes. + pub(crate) fn word_len_for_bytes(byte_len: usize) -> usize { + byte_len.div_ceil(WORD_BYTES) + } + + /// Pack bytes into big-endian `U256` words, zero-padding the final word. + pub(crate) fn encode_words(bytes: &[u8]) -> Vec { + let mut padded = bytes.to_vec(); + padded.resize(Self::word_len_for_bytes(bytes.len()) * WORD_BYTES, 0); + padded.chunks(WORD_BYTES).map(U256::from_be_slice).collect::>() + } + + /// Decode packed words and verify that all unused padding bytes are zero. + #[cfg(test)] + pub(crate) fn decode_words(words: &[U256], byte_len: usize) -> Result, String> { + let capacity = words.len() * WORD_BYTES; + if byte_len > capacity { + return Err(format!( + "packed program byte length {byte_len} exceeds word capacity {capacity}" + )); + } + + let mut bytes = Vec::with_capacity(capacity); + for word in words { + bytes.extend_from_slice(&word.to_be_bytes::<32>()); + } + + if bytes[byte_len..].iter().any(|byte| *byte != 0) { + return Err("packed program has non-zero padding bytes".to_string()); + } + + bytes.truncate(byte_len); + Ok(bytes) + } +} + +#[cfg(test)] +mod tests { + use ruint::aliases::U256; + + use super::{PackedProgramCodec, PayloadSectionKind, VkPayloadLayout}; + + #[test] + fn vk_payload_layout_reserves_monotonic_sections() { + let layout = VkPayloadLayout::for_vk(31, 3, 5, 7, 2).unwrap(); + + assert_eq!(layout.word_offset(PayloadSectionKind::Header).unwrap(), 0); + assert_eq!( + layout.word_offset(PayloadSectionKind::QuotientConstants).unwrap(), + 31 + ); + assert_eq!( + layout.word_offset(PayloadSectionKind::QuotientProgram).unwrap(), + 34 + ); + assert_eq!( + layout.word_offset(PayloadSectionKind::FixedCommitments).unwrap(), + 39 + ); + assert_eq!( + layout.word_offset(PayloadSectionKind::PermutationCommitments).unwrap(), + 67 + ); + assert_eq!(layout.total_words(), 75); + assert_eq!( + layout.byte_offset(PayloadSectionKind::FixedCommitments).unwrap(), + 39 * 0x20 + ); + assert_eq!( + layout.byte_len(PayloadSectionKind::PermutationCommitments).unwrap(), + 2 * 0x80 + ); + } + + #[test] + fn vk_payload_layout_rejects_duplicate_sections() { + let mut layout = VkPayloadLayout::new(); + layout + .reserve( + PayloadSectionKind::Header, + crate::lowering::layout::VK_HEADER_WORDS, + ) + .unwrap(); + let err = layout + .reserve(PayloadSectionKind::Header, 1) + .expect_err("duplicate section rejected"); + + assert!(err.contains("duplicate VK payload section")); + } + + #[test] + fn packed_program_codec_round_trips_with_explicit_byte_len() { + let bytes = (0..37u8).collect::>(); + let words = PackedProgramCodec::encode_words(&bytes); + + assert_eq!(words.len(), 2); + assert_eq!( + PackedProgramCodec::decode_words(&words, bytes.len()).unwrap(), + bytes + ); + } + + #[test] + fn packed_program_codec_rejects_length_past_capacity() { + let words = PackedProgramCodec::encode_words(&[1, 2, 3]); + let err = PackedProgramCodec::decode_words(&words, 33) + .expect_err("byte length past capacity rejected"); + + assert!(err.contains("exceeds word capacity")); + } + + #[test] + fn packed_program_codec_rejects_non_zero_padding() { + let bytes = vec![0xaa; 31]; + let mut words = PackedProgramCodec::encode_words(&bytes); + words[0] |= U256::from(1u64); + + let err = PackedProgramCodec::decode_words(&words, bytes.len()) + .expect_err("non-zero padding rejected"); + + assert!(err.contains("non-zero padding")); + } +} diff --git a/proofs/solidity-verifier/src/lowering/mod.rs b/proofs/solidity-verifier/src/lowering/mod.rs new file mode 100644 index 000000000..40b6f4e00 --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/mod.rs @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Private lowering pipeline for Solidity verifier artifact construction. +//! +//! `lowering` owns the reusable planning and emission machinery: protocol +//! planning, ABI/layout facts, proof encoders, VK payload generation, KZG and +//! quotient-numerator emitters, calldata repacking, artifact assembly, and +//! Askama render models. The public `SolidityGenerator` facade in `builder` +//! delegates here with a concrete, read-only [`VerifierBuildInputs`] value. + +use midnight_curves::{Bls12, Fq}; +use midnight_proofs::{ + plonk::VerifyingKey, + poly::kzg::{params::ParamsKZG, KZGCommitmentScheme}, +}; + +use crate::api::AccumulatorEncoding; + +pub(crate) mod abi; +pub(crate) mod artifacts; +pub(crate) mod calldata; +pub(crate) mod config; +pub(crate) mod diagnostics; +pub(crate) mod encoding; +pub(crate) mod kzg; +pub(crate) mod layout; +pub(crate) mod plan; +pub(crate) mod protocol; +pub(crate) mod quotient; +pub(crate) mod quotient_numerator; +pub(crate) mod render; +pub(crate) mod vk; + +use encoding::ConstraintSystemMeta; + +#[derive(Clone, Copy, Debug)] +pub(crate) struct VerifierBuildInputs<'params, 'meta> { + pub(crate) params: &'params ParamsKZG, + pub(crate) vk: &'params VerifyingKey>, + pub(crate) num_instances: usize, + pub(crate) num_committed_instances: usize, + pub(crate) acc_encoding: Option, + pub(crate) meta: &'meta ConstraintSystemMeta, +} + +#[cfg(all(test, feature = "evm"))] +pub(crate) use quotient_numerator::vm::RepackedProofScalarLayout; + +#[cfg(test)] +mod tests; diff --git a/proofs/solidity-verifier/src/lowering/plan.rs b/proofs/solidity-verifier/src/lowering/plan.rs new file mode 100644 index 000000000..e1682f287 --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/plan.rs @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Converged lowering plan for one Solidity verifier build. +//! +//! Rendering, diagnostics, and proof repacking all need the same finalized +//! VK payload, proof layout, memory layout, and quotient program facts. This +//! module makes that post-convergence state explicit so call sites do not +//! independently rebuild small slices of the verifier shape. + +use crate::lowering::{ + abi::ProofCalldataLayout, + encoding::{ConstraintSystemMeta, Data, Ptr}, + kzg, layout, + layout::memory::{PcsMemoryRequirements, VerifierMemoryLayout, VerifierMemoryLayoutConfig}, + quotient::{QuotientComputationBlocks, QuotientHelperFlags, QuotientStateSlots}, + quotient_numerator::vm::{QuotientProgramBuild, QuotientProgramPlan, RepackedProofLayoutPlan}, + render::{Halo2VerifyingKey, QuotientExternal, QuotientProgram}, + VerifierBuildInputs, +}; + +/// Finalized, reusable codegen facts for one verifier render/repack operation. +#[derive(Debug)] +pub(crate) struct LoweringPlan { + pub(crate) vk: Halo2VerifyingKey, + pub(crate) vk_mptr: Ptr, + pub(crate) meta: ConstraintSystemMeta, + pub(crate) data: Data, + pub(crate) proof_layout: ProofCalldataLayout, + pub(crate) quotient: PlannedQuotient, + pub(crate) pcs_memory_requirements: PcsMemoryRequirements, + pub(crate) memory: VerifierMemoryLayout, +} + +/// Quotient VM and rendering metadata tied to the finalized memory layout. +#[derive(Clone, Debug)] +pub(crate) struct PlannedQuotient { + pub(crate) plan: QuotientProgramPlan, + pub(crate) build: QuotientProgramBuild, + pub(crate) program: QuotientProgram, + pub(crate) stack_mptr: usize, + pub(crate) state_slots: QuotientStateSlots, + pub(crate) sorted_simple: Vec, +} + +/// Quotient rendering mode for the main verifier. +#[derive(Clone, Debug)] +pub(crate) enum QuotientRendering { + /// The main verifier delegates quotient reconstruction to a pinned + /// evaluator and therefore renders no local quotient VM program. + External { external: QuotientExternal }, + /// The main verifier runs the compact quotient VM and any native callbacks + /// locally. + Compact { + blocks: Box, + program: QuotientProgram, + }, +} + +impl QuotientRendering { + /// Helper flags needed by the Solidity/Yul templates. + pub(crate) fn helper_flags(&self) -> QuotientHelperFlags { + match self { + Self::External { .. } => QuotientComputationBlocks::default().helper_flags(), + Self::Compact { blocks, .. } => blocks.helper_flags(), + } + } + + /// Decompose into the legacy template fields from one coordinated value. + pub(crate) fn into_template_parts( + self, + ) -> ( + Option, + QuotientComputationBlocks, + Option, + ) { + match self { + Self::External { external } => { + (Some(external), QuotientComputationBlocks::default(), None) + } + Self::Compact { blocks, program } => (None, *blocks, Some(program)), + } + } +} + +/// Quotient rendering facts for the standalone evaluator artifact. +#[derive(Clone, Debug)] +pub(crate) struct QuotientEvaluatorRendering { + pub(crate) external: QuotientExternal, + pub(crate) blocks: QuotientComputationBlocks, + pub(crate) program: QuotientProgram, +} + +impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { + /// Build the finalized lowering plan for this concrete verifier. + pub(crate) fn lowering_plan(&self) -> LoweringPlan { + LoweringPlan::new(self) + } +} + +impl LoweringPlan { + /// Build a finalized plan, preserving the existing bounded convergence + /// process while exposing the resulting facts as one value. + pub(crate) fn new(inputs: &VerifierBuildInputs<'_, '_>) -> Self { + let proof_cptr = Ptr::calldata(layout::abi::VERIFY_PROOF_PROOF_CPTR); + let vk = inputs.generate_vk(); + let (vk_mptr, meta, data, _) = inputs.meta_data_for_stable_static_layout(&vk); + let proof_layout = ProofCalldataLayout::from_protocol( + &meta.protocol, + proof_cptr.value().as_usize(), + meta.num_evals, + meta.num_point_sets, + ); + + let quotient_plan = inputs.quotient_program_plan(&meta, &data); + let sorted_simple = quotient_plan.sorted_simple.clone(); + let quotient_program_build = + inputs.build_quotient_program_items("ient_plan.items, "ient_plan.selector_fold); + let native_callback_scratch_words = + inputs.native_callback_scratch_words(&meta, "ient_plan); + let quotient_stack_words = VerifierBuildInputs::quotient_stack_words_for_build( + "ient_program_build, + native_callback_scratch_words, + ); + let quotient_state_words = + VerifierBuildInputs::quotient_state_words("ient_plan.selector_fold); + let pcs_memory_requirements = kzg::memory_requirements(&meta, &data); + let memory = inputs.memory_layout_for( + &meta, + &vk, + vk_mptr, + VerifierMemoryLayoutConfig { + quotient_state_words, + quotient_stack_words, + acc_msm_terms: Self::acc_msm_terms(inputs), + pcs: pcs_memory_requirements, + ..VerifierMemoryLayoutConfig::default() + }, + ); + let (quotient_program, quotient_stack_mptr, quotient_state_slots) = inputs + .quotient_template_program( + quotient_program_build.clone(), + &vk, + vk_mptr, + &memory, + "ient_plan.selector_fold, + ); + + let plan = Self { + vk, + vk_mptr, + meta, + data, + proof_layout, + quotient: PlannedQuotient { + plan: quotient_plan, + build: quotient_program_build, + program: quotient_program, + stack_mptr: quotient_stack_mptr, + state_slots: quotient_state_slots, + sorted_simple, + }, + pcs_memory_requirements, + memory, + }; + plan.validate_generator_invariants() + .unwrap_or_else(|err| panic!("generator invariant violation: {err}")); + plan + } + + /// Repacking plan derived from the same proof layout used for rendering. + pub(crate) fn repacked_proof_layout_plan(&self) -> RepackedProofLayoutPlan { + RepackedProofLayoutPlan::from_proof_layout(&self.proof_layout) + } + + /// Build quotient rendering for the main verifier. + pub(crate) fn quotient_rendering( + &self, + inputs: &VerifierBuildInputs<'_, '_>, + trace: bool, + external_quotient: bool, + ) -> QuotientRendering { + if external_quotient { + return QuotientRendering::External { + external: self.quotient_external_frame(), + }; + } + + QuotientRendering::Compact { + blocks: Box::new(self.compact_quotient_blocks(inputs, trace)), + program: self.quotient.program.clone(), + } + } + + /// Build quotient rendering for the standalone evaluator artifact. + pub(crate) fn quotient_evaluator_rendering( + &self, + inputs: &VerifierBuildInputs<'_, '_>, + trace: bool, + ) -> QuotientEvaluatorRendering { + QuotientEvaluatorRendering { + external: self.quotient_external_frame(), + blocks: self.compact_quotient_blocks(inputs, trace), + program: self.quotient.program.clone(), + } + } + + /// Expected external evaluator frame for this finalized layout. + pub(crate) fn quotient_external_frame(&self) -> QuotientExternal { + VerifierBuildInputs::quotient_external_frame( + self.vk_mptr, + self.vk.len(), + &self.meta, + &self.memory, + self.quotient.sorted_simple.len(), + ) + } + + /// Generate local compact-VM/native-callback Yul blocks for inline renders. + fn compact_quotient_blocks( + &self, + inputs: &VerifierBuildInputs<'_, '_>, + trace: bool, + ) -> QuotientComputationBlocks { + inputs.compact_quotient_computation_blocks( + &self.meta, + &self.data, + &self.quotient.plan, + self.quotient.stack_mptr, + self.quotient.state_slots, + trace, + ) + } + + /// Re-check cross-module invariants after convergence. + /// + /// This catches drift between independently computed protocol, memory, + /// quotient, and precompile-coverage facts before any Solidity template is + /// rendered. + fn validate_generator_invariants(&self) -> Result<(), String> { + self.meta + .validate_against_protocol() + .map_err(|err| format!("protocol invariant failed: {err}"))?; + self.memory + .validate() + .map_err(|err| format!("memory-region non-overlap invariant failed: {err}"))?; + kzg::validate_absorbed_g1_precompile_coverage( + &self.meta, + &self.data, + &self.memory, + &self.proof_layout, + ) + .map_err(|err| format!("absorbed-G1 precompile coverage invariant failed: {err}"))?; + let planned_pcs = kzg::memory_requirements(&self.meta, &self.data); + if self.pcs_memory_requirements != planned_pcs { + return Err(format!( + "PCS memory requirements drifted: planned {:?}, recomputed {:?}", + self.pcs_memory_requirements, planned_pcs + )); + } + if self.quotient.program.len != self.quotient.build.bytes.len() { + return Err(format!( + "quotient program length drifted: model={} build={}", + self.quotient.program.len, + self.quotient.build.bytes.len() + )); + } + if self.quotient.build.consts.len() > self.vk.quotient_const_words { + return Err(format!( + "quotient const table exceeds VK reservation: consts={} words={}", + self.quotient.build.consts.len(), + self.vk.quotient_const_words + )); + } + let quotient_program_words = layout::vk_payload::PackedProgramCodec::word_len_for_bytes( + self.quotient.build.bytes.len(), + ); + if quotient_program_words > self.vk.quotient_program_words { + return Err(format!( + "quotient bytecode exceeds VK reservation: program_words={quotient_program_words} reserved={}", + self.vk.quotient_program_words + )); + } + if self.quotient.program.stack_mptr != self.quotient.stack_mptr { + return Err(format!( + "quotient stack pointer drifted: model={:#x} planned={:#x}", + self.quotient.program.stack_mptr, self.quotient.stack_mptr + )); + } + if self.quotient.program.eval_numer_mptr != self.quotient.state_slots.eval_numer_mptr { + return Err(format!( + "quotient state pointer drifted: model={:#x} planned={:#x}", + self.quotient.program.eval_numer_mptr, self.quotient.state_slots.eval_numer_mptr + )); + } + Ok(()) + } + + /// Number of `(G1, scalar)` terms required by the optional accumulator MSM. + fn acc_msm_terms(inputs: &VerifierBuildInputs<'_, '_>) -> usize { + inputs + .acc_encoding + .map(|acc_encoding| { + let fixed_scalar_count = acc_encoding + .fixed_scalar_count(inputs.num_instances) + .expect("accumulator encoding validated by GeneratorConfig"); + fixed_scalar_count + 1 + }) + .unwrap_or(0) + } +} diff --git a/proofs/solidity-verifier/src/lowering/protocol/mod.rs b/proofs/solidity-verifier/src/lowering/protocol/mod.rs new file mode 100644 index 000000000..a035abaf5 --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/protocol/mod.rs @@ -0,0 +1,1223 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Typed verifier protocol plan. +//! +//! This module is deliberately small and local. It ports the useful shape of +//! `snark-verifier`'s protocol/query planning into this generator without +//! adopting its loader or unrolled EVM output. The Askama/Yul emitters still +//! decide how to render the verifier, but they consume one validated source of +//! truth for proof reads, quotient topology, common polynomial needs, and PCS +//! query order. + +use std::collections::BTreeSet; + +use itertools::Itertools; +use midnight_curves::Fq; +#[cfg(test)] +use midnight_proofs::plonk::Expression; +use midnight_proofs::plonk::{Any, Column, ConstraintSystem}; + +use crate::lowering::layout::trace; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct QueryKey { + /// Column index within its column kind. + pub(crate) column: usize, + /// Halo2 rotation relative to the current row. + pub(crate) rotation: i32, +} + +impl QueryKey { + /// Construct a query key from a column index and rotation. + pub(crate) fn new(column: usize, rotation: i32) -> Self { + Self { column, rotation } + } + + /// Return the historical tuple representation used by + /// `ConstraintSystemMeta`. + pub(crate) fn tuple(self) -> (usize, i32) { + (self.column, self.rotation) + } +} + +/// Kinds of G1 commitments read from proof calldata. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum CommitmentRead { + // The commitment stream only needs kinds for sizing and transcript order. + // Column/query identity lives in the eval and PCS plans below. + Advice, + LookupMultiplicity, + PermutationProduct, + LookupHelper, + LookupAccumulator, + Trash, + Quotient, +} + +impl CommitmentRead { + /// Return whether this read is one of the quotient limb commitments. + pub(crate) fn is_quotient(self) -> bool { + matches!(self, Self::Quotient) + } +} + +/// Which evaluation of a permutation product commitment is being read. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum PermutationZEval { + /// Evaluation at `x`. + Cur, + /// Evaluation at `omega * x`. + Next, + /// Evaluation at the last usable rotation. + Last, +} + +/// Scalar evaluations read from the proof's main eval block. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum EvalRead { + /// PCS-opened committed instance column. + CommittedInstance(QueryKey), + /// Advice polynomial query. + Advice(QueryKey), + /// Non-simple fixed polynomial query. + Fixed(QueryKey), + /// Common permutation sigma polynomial query. + PermutationCommon { column: Column }, + /// Permutation product query. + PermutationZ { set: usize, kind: PermutationZEval }, + /// LogUp multiplicity polynomial query. + LookupMultiplicity { lookup: usize }, + /// LogUp helper polynomial query. + LookupHelper { lookup: usize, chunk: usize }, + /// LogUp accumulator polynomial query. + LookupAccumulator { lookup: usize, rotation: i32 }, + /// Trashcan accumulator query. + Trash { index: usize }, +} + +/// Source of one KZG multi-open query. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum PcsQuerySource { + Advice(QueryKey), + CommittedInstance(QueryKey), + PermutationZ { set: usize, kind: PermutationZEval }, + LookupMultiplicity { lookup: usize }, + LookupHelper { lookup: usize, chunk: usize }, + LookupAccumulator { lookup: usize, rotation: i32 }, + Trash { index: usize }, + Fixed(QueryKey), + PermutationCommon { column: Column }, + Linearization, +} + +impl PcsQuerySource { + /// Return the rotation point used by this query. + /// + /// `rotation_last` is circuit-dependent and is needed only for the + /// permutation product's last-row opening. + pub(crate) fn rotation(self, rotation_last: i32) -> i32 { + match self { + Self::Advice(q) | Self::CommittedInstance(q) | Self::Fixed(q) => q.rotation, + Self::PermutationZ { kind, .. } => match kind { + PermutationZEval::Cur => 0, + PermutationZEval::Next => 1, + PermutationZEval::Last => rotation_last, + }, + Self::LookupMultiplicity { .. } + | Self::LookupHelper { .. } + | Self::Trash { .. } + | Self::PermutationCommon { .. } + | Self::Linearization => 0, + Self::LookupAccumulator { rotation, .. } => rotation, + } + } +} + +/// Common polynomial values generated locally by the verifier. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) enum CommonPoly { + /// Rotation point `omega^rotation * x`. + Rotation(i32), + /// First-row Lagrange basis evaluation. + L0, + /// Last-row Lagrange basis evaluation. + LLast, + /// Blinding-row Lagrange basis evaluation. + LBlind, +} + +/// Ordered proof-read plan for commitments and scalar evaluations. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub(crate) struct ProofReadPlan { + /// G1 commitment reads in transcript order. + pub(crate) commitments: Vec, + /// Scalar evaluation reads in proof order. + pub(crate) evals: Vec, +} + +/// Counts of quotient identities by source family. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub(crate) struct QuotientIdentityPlan { + /// Gate polynomial identities. + pub(crate) gates: usize, + /// Permutation identities. + pub(crate) permutation: usize, + /// Lookup identities. + pub(crate) lookup: usize, + /// Trashcan identities. + pub(crate) trash: usize, +} + +impl QuotientIdentityPlan { + /// Total identity count in the global `y` fold. + pub(crate) fn total(&self) -> usize { + self.gates + self.permutation + self.lookup + self.trash + } +} + +/// Trace namespace for quotient identities, re-exported for protocol tests. +pub(crate) const TRACE_QUOTIENT_IDENTITY_BASE: u64 = trace::QUOTIENT_IDENTITY_BASE; +/// Trace namespace for KZG query serialization, re-exported for protocol tests. +pub(crate) const TRACE_PCS_QUERY_BASE: u64 = trace::PCS_QUERY_BASE; + +#[cfg(test)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub(crate) struct UsedQueries { + /// Fixed queries referenced by an expression. + pub(crate) fixed: BTreeSet, + /// Advice queries referenced by an expression. + pub(crate) advice: BTreeSet, + /// Instance queries referenced by an expression. + pub(crate) instance: BTreeSet, + /// Challenge indices referenced by an expression. + pub(crate) challenges: BTreeSet, +} + +#[cfg(test)] +impl UsedQueries { + /// Merge two expression-use summaries. + fn merge(mut self, other: Self) -> Self { + self.fixed.extend(other.fixed); + self.advice.extend(other.advice); + self.instance.extend(other.instance); + self.challenges.extend(other.challenges); + self + } +} + +/// Collect the polynomial/challenge queries an expression references. +/// +/// This is the local analogue of snark-verifier's expression visitors: the +/// generator can validate and lower expressions without string matching the +/// emitted Yul. +#[cfg(test)] +pub(crate) fn used_query(expression: &Expression) -> UsedQueries { + expression.evaluate( + &|_| UsedQueries::default(), + &|_| UsedQueries::default(), + &|query| { + let mut used = UsedQueries::default(); + used.fixed.insert(QueryKey::new(query.column_index(), query.rotation().0)); + used + }, + &|query| { + let mut used = UsedQueries::default(); + used.advice.insert(QueryKey::new(query.column_index(), query.rotation().0)); + used + }, + &|query| { + let mut used = UsedQueries::default(); + used.instance.insert(QueryKey::new(query.column_index(), query.rotation().0)); + used + }, + &|challenge| { + let mut used = UsedQueries::default(); + used.challenges.insert(challenge.index()); + used + }, + &|inner| inner, + &|lhs, rhs| lhs.merge(rhs), + &|lhs, rhs| lhs.merge(rhs), + &|inner, _| inner, + ) +} + +/// Return which locally-computed Lagrange polynomials are needed. +pub(crate) fn used_lagrange( + uses_permutation: bool, + uses_lookup: bool, + uses_trash: bool, +) -> BTreeSet { + let mut out = BTreeSet::new(); + if uses_permutation || uses_lookup { + out.insert(CommonPoly::L0); + out.insert(CommonPoly::LLast); + out.insert(CommonPoly::LBlind); + } + if uses_trash { + out.insert(CommonPoly::Rotation(0)); + } + out +} + +/// Complete protocol-shape plan consumed by proof layout, memory layout, and +/// emitters. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub(crate) struct ProtocolPlan { + /// Number of fixed columns in the constraint system. + pub(crate) num_fixeds: usize, + /// Columns participating in the permutation argument. + pub(crate) permutation_columns: Vec>, + /// Maximum columns per permutation product chunk. + pub(crate) permutation_chunk_len: usize, + /// Helper chunks per lookup argument. + pub(crate) lookup_chunks: Vec, + /// Number of lookup arguments. + pub(crate) num_lookups: usize, + /// Number of trashcan arguments. + pub(crate) num_trashcans: usize, + /// Number of permutation product commitments. + pub(crate) num_permutation_zs: usize, + /// Number of quotient limb commitments. + pub(crate) num_quotients: usize, + /// Advice queries in upstream verifier order. + pub(crate) advice_queries: Vec, + /// Instance queries in upstream verifier order. + pub(crate) instance_queries: Vec, + /// Number of simple selector columns. + pub(crate) num_simple_selectors: usize, + /// Fixed-column indices that are simple selectors. + pub(crate) simple_selector_cols: BTreeSet, + /// Number of instance columns opened through PCS. + pub(crate) num_committed_instances: usize, + /// Advice commitment counts by user phase. + pub(crate) num_user_advices: Vec, + /// Challenge counts by user phase. + pub(crate) num_user_challenges: Vec, + /// Advice-column remapping from CS order into phase-order commitment order. + pub(crate) advice_indices: Vec, + /// Challenge-index remapping from CS order into phase-order memory slots. + pub(crate) challenge_indices: Vec, + /// Last negative row rotation used by blinding/last-row checks. + pub(crate) rotation_last: i32, + /// Commitment/evaluation proof-read schedule. + pub(crate) proof: ProofReadPlan, + /// PCS query schedule, ending with the synthetic linearization query. + pub(crate) pcs_queries: Vec, + /// Trace topic ids for quotient identities. + pub(crate) quotient_trace_ids: Vec, + /// Trace topic ids for PCS queries. + pub(crate) pcs_query_trace_ids: Vec, + /// Common polynomial values the generated verifier must materialize. + pub(crate) common_polys: BTreeSet, + /// Quotient identity counts by family. + pub(crate) quotient: QuotientIdentityPlan, +} + +impl ProtocolPlan { + /// Build the generated verifier's proof-read and PCS-query plan from the + /// Midfall constraint system. + /// + /// This is the codegen-time counterpart of the iterator-heavy verifier + /// flow in `midfall/proofs/src/plonk/verifier.rs`: hash/read advice + /// commitments by phase, read lookup/permutation/trash commitments, read + /// quotient limb commitments, sample `x`, then read or compute the evals + /// passed to `partially_evaluate_identities` and KZG `multi_prepare`. + /// The plan preserves that order so the Solidity transcript and proof + /// cursors stay byte-compatible with the Rust verifier. + pub(crate) fn from_constraint_system( + cs: &ConstraintSystem, + nb_committed_instances: usize, + ) -> Self { + let cs_degree = cs.degree(); + let num_fixeds = cs.num_fixed_columns(); + let permutation_columns = cs.permutation().get_columns(); + let permutation_chunk_len = cs_degree - 2; + let num_permutation_zs = if permutation_columns.is_empty() { + 0 + } else { + permutation_columns.len().div_ceil(permutation_chunk_len) + }; + + let lookup_chunks: Vec = cs + .lookups() + .iter() + .map(|lookup| lookup.chunk_by_degree(cs_degree).num_chunks()) + .collect(); + let num_lookups = lookup_chunks.len(); + let num_trashcans = cs.trashcans().len(); + let num_quotients = if crate::OUTER_SINGLE_H_COMMITMENT_ENABLED { + 1 + } else { + cs_degree.saturating_sub(1) + }; + + let advice_queries = cs + .advice_queries() + .iter() + .map(|(column, rotation)| QueryKey::new(column.index(), rotation.0)) + .collect_vec(); + let fixed_queries = cs + .fixed_queries() + .iter() + .map(|(column, rotation)| QueryKey::new(column.index(), rotation.0)) + .collect_vec(); + let instance_queries = cs + .instance_queries() + .iter() + .map(|(column, rotation)| QueryKey::new(column.index(), rotation.0)) + .collect_vec(); + + let num_simple_selectors = cs.num_simple_selectors(); + let simple_selector_cols: BTreeSet = + (0..num_fixeds).filter(|&idx| cs.has_simple_selector_col(idx)).collect(); + + let max_advice_phase = cs.advice_column_phase().iter().copied().max().unwrap_or(0); + let max_challenge_phase = cs.challenge_phase().iter().copied().max().unwrap_or(0); + let num_phase = max_advice_phase.max(max_challenge_phase) as usize + 1; + let remapping = |phase: Vec| { + // Midnight stores advice/challenge columns in declaration order but + // verifies them phase by phase. `nums` gives per-phase counts and + // `index` maps original column indices into that phase-packed order. + let nums = phase.iter().fold(vec![0usize; num_phase], |mut nums, p| { + nums[*p as usize] += 1; + nums + }); + let offsets = nums.iter().take(num_phase - 1).fold(vec![0usize], |mut offsets, n| { + offsets.push(offsets.last().unwrap() + n); + offsets + }); + let index = phase + .iter() + .scan(offsets, |state, p| { + let i = state[*p as usize]; + state[*p as usize] += 1; + Some(i) + }) + .collect::>(); + (nums, index) + }; + let (num_user_advices, advice_indices) = remapping(cs.advice_column_phase()); + let (num_user_challenges, challenge_indices) = remapping(cs.challenge_phase()); + + let rotation_last = -(cs.blinding_factors() as i32 + 1); + let mut proof = ProofReadPlan::default(); + + // Hash the prover's advice commitments into the transcript by phase, + // squeezing the phase challenge before the next phase's commitments + // are read (`parse_trace`). + proof + .commitments + .extend((0..advice_indices.len()).map(|_| CommitmentRead::Advice)); + // Lookup commitments follow the Rust verifier order: one LogUp + // multiplicity commitment per lookup, then each chunk helper and + // accumulator commitment after permutation products are bound. + proof + .commitments + .extend((0..num_lookups).map(|_| CommitmentRead::LookupMultiplicity)); + // The verifier samples beta/gamma before hashing each permutation + // product commitment; this plan keeps only the proof cursor order, + // while the Solidity template owns the transcript squeezes. + proof + .commitments + .extend((0..num_permutation_zs).map(|_| CommitmentRead::PermutationProduct)); + for &chunks in &lookup_chunks { + proof.commitments.extend((0..chunks).map(|_| CommitmentRead::LookupHelper)); + proof.commitments.push(CommitmentRead::LookupAccumulator); + } + proof.commitments.extend((0..num_trashcans).map(|_| CommitmentRead::Trash)); + // Read commitment(s) to the quotient polynomial h(X)=nu(X)/(X^n-1). + // The outer single-H feature mirrors midnight-proofs' one-commitment + // transcript shape; otherwise h is split into degree-bound limbs. + proof.commitments.extend((0..num_quotients).map(|_| CommitmentRead::Quotient)); + + // Committed-instance columns are opened by PCS and read as proof + // evals. Non-committed instance columns are Lagrange-interpolated + // locally from public inputs and therefore do not appear here. + proof.evals.extend( + instance_queries + .iter() + .copied() + .filter(|q| q.column < nb_committed_instances) + .map(EvalRead::CommittedInstance), + ); + proof.evals.extend(advice_queries.iter().copied().map(EvalRead::Advice)); + // Read (num_fixed_columns - num_simple_selectors) fixed evaluations. + // Simple selector columns are intentionally absent from the proof + // scalar stream and are filled by the quotient/linearization path. + proof.evals.extend( + fixed_queries + .iter() + .copied() + .filter(|q| !simple_selector_cols.contains(&q.column)) + .map(EvalRead::Fixed), + ); + // The permutation argument opens its product commitments at x and + // omega*x, plus omega^last*x for every set except the final one. + proof.evals.extend( + permutation_columns + .iter() + .copied() + .map(|column| EvalRead::PermutationCommon { column }), + ); + for set in 0..num_permutation_zs { + proof.evals.push(EvalRead::PermutationZ { + set, + kind: PermutationZEval::Cur, + }); + proof.evals.push(EvalRead::PermutationZ { + set, + kind: PermutationZEval::Next, + }); + if set + 1 != num_permutation_zs { + proof.evals.push(EvalRead::PermutationZ { + set, + kind: PermutationZEval::Last, + }); + } + } + for (lookup, &chunks) in lookup_chunks.iter().enumerate() { + proof.evals.push(EvalRead::LookupMultiplicity { lookup }); + proof + .evals + .extend((0..chunks).map(move |chunk| EvalRead::LookupHelper { lookup, chunk })); + proof.evals.push(EvalRead::LookupAccumulator { + lookup, + rotation: 0, + }); + proof.evals.push(EvalRead::LookupAccumulator { + lookup, + rotation: 1, + }); + } + proof.evals.extend((0..num_trashcans).map(|index| EvalRead::Trash { index })); + + // Collect the queries checked in the KZG multi-open. + // Queries corresponding to simple, multiplicative selectors need not be checked + // directly; `compute_linearization_commitment` reintroduces + // those selector commitments through the custom linearization query. + let mut pcs_queries: Vec = Vec::new(); + pcs_queries.extend(advice_queries.iter().copied().map(PcsQuerySource::Advice)); + pcs_queries.extend( + instance_queries + .iter() + .copied() + .filter(|q| q.column < nb_committed_instances) + .map(PcsQuerySource::CommittedInstance), + ); + for set in 0..num_permutation_zs { + pcs_queries.push(PcsQuerySource::PermutationZ { + set, + kind: PermutationZEval::Cur, + }); + pcs_queries.push(PcsQuerySource::PermutationZ { + set, + kind: PermutationZEval::Next, + }); + if set + 1 != num_permutation_zs { + pcs_queries.push(PcsQuerySource::PermutationZ { + set, + kind: PermutationZEval::Last, + }); + } + } + for (lookup, &chunks) in lookup_chunks.iter().enumerate() { + pcs_queries.push(PcsQuerySource::LookupMultiplicity { lookup }); + pcs_queries.extend( + (0..chunks).map(move |chunk| PcsQuerySource::LookupHelper { lookup, chunk }), + ); + pcs_queries.push(PcsQuerySource::LookupAccumulator { + lookup, + rotation: 0, + }); + pcs_queries.push(PcsQuerySource::LookupAccumulator { + lookup, + rotation: 1, + }); + } + pcs_queries.extend((0..num_trashcans).map(|index| PcsQuerySource::Trash { index })); + pcs_queries.extend( + fixed_queries + .iter() + .copied() + .filter(|q| !simple_selector_cols.contains(&q.column)) + .map(PcsQuerySource::Fixed), + ); + pcs_queries.extend( + permutation_columns + .iter() + .copied() + .map(|column| PcsQuerySource::PermutationCommon { column }), + ); + pcs_queries.push(PcsQuerySource::Linearization); + + let mut common_polys = used_lagrange( + num_permutation_zs != 0, + num_lookups != 0, + num_trashcans != 0, + ); + // `verify_algebraic_constraints` samples x after all proof + // commitments, then every planned PCS query opens at + // omega^rotation*x (including x itself at rotation zero). + common_polys.extend( + pcs_queries + .iter() + .copied() + .map(|query| CommonPoly::Rotation(query.rotation(rotation_last))), + ); + + let permutation_identity_count = if num_permutation_zs == 0 { + 0 + } else { + 2 + (num_permutation_zs.saturating_sub(1)) + num_permutation_zs + }; + let lookup_identity_count = lookup_chunks.iter().map(|chunks| chunks + 2).sum(); + let quotient = QuotientIdentityPlan { + gates: cs.gates().iter().map(|gate| gate.polynomials().len()).sum(), + permutation: permutation_identity_count, + lookup: lookup_identity_count, + trash: num_trashcans, + }; + let quotient_trace_ids = (0..quotient.total()) + .map(|idx| TRACE_QUOTIENT_IDENTITY_BASE + idx as u64) + .collect::>(); + let pcs_query_trace_ids = (0..pcs_queries.len()) + .map(|idx| TRACE_PCS_QUERY_BASE + idx as u64) + .collect::>(); + + let plan = Self { + num_fixeds, + permutation_columns, + permutation_chunk_len, + lookup_chunks, + num_lookups, + num_trashcans, + num_permutation_zs, + num_quotients, + advice_queries, + instance_queries, + num_simple_selectors, + simple_selector_cols, + num_committed_instances: nb_committed_instances, + num_user_advices, + num_user_challenges, + advice_indices, + challenge_indices, + rotation_last, + proof, + pcs_queries, + quotient_trace_ids, + pcs_query_trace_ids, + common_polys, + quotient, + }; + plan.validate().unwrap_or_else(|err| panic!("invalid protocol plan: {err}")); + plan + } + + /// Number of scalar evaluations in the proof's main eval block. + pub(crate) fn num_main_evals(&self) -> usize { + self.proof.evals.len() + } + + /// Number of proof commitment reads, including quotient limbs. + pub(crate) fn num_commitments(&self) -> usize { + self.proof.commitments.len() + } + + /// Number of proof commitment reads before quotient limbs. + pub(crate) fn num_non_quotient_commitments(&self) -> usize { + self.proof + .commitments + .iter() + .filter(|commitment| !commitment.is_quotient()) + .count() + } + + /// Commitment group sizes in the exact proof/transcript order used by + /// the generated verifier and the off-chain proof repacker. + pub(crate) fn commitment_read_groups(&self) -> Vec { + let mut groups = Vec::new(); + groups.extend(self.num_user_advices.iter().copied().filter(|&n| n != 0)); + if self.num_lookups != 0 { + groups.push(self.num_lookups); + } + if self.num_permutation_zs != 0 { + groups.push(self.num_permutation_zs); + } + for &chunks in &self.lookup_chunks { + groups.push(chunks); + groups.push(1); + } + if self.num_trashcans != 0 { + groups.push(self.num_trashcans); + } + groups.push(self.num_quotients); + debug_assert_eq!( + groups.iter().sum::(), + self.proof.commitments.len(), + "protocol commitment groups must cover the proof commitment plan" + ); + groups + } + + /// Map a source-local lookup identity index to `(lookup_index, + /// identity_index_within_lookup)`. + /// + /// LogUp emits `boundary`, one helper identity per chunk, and an + /// accumulator identity for each lookup, so the stride is + /// `lookup_chunks[i] + 2`, not a constant. + pub(crate) fn lookup_identity_source(&self, identity_index: usize) -> Option<(usize, usize)> { + let mut base = 0usize; + for (lookup, &chunks) in self.lookup_chunks.iter().enumerate() { + let count = chunks + 2; + if identity_index < base + count { + return Some((lookup, identity_index - base)); + } + base += count; + } + None + } + + /// Check cross-field invariants of the protocol plan. + /// + /// This catches stale assumptions when upstream `midnight-proofs` changes + /// proof-read order, selector handling, or PCS query coverage. + pub(crate) fn validate(&self) -> Result<(), String> { + if self.num_simple_selectors != self.simple_selector_cols.len() { + return Err(format!( + "simple selector count mismatch: count={} cols={}", + self.num_simple_selectors, + self.simple_selector_cols.len() + )); + } + + let quotient_count = self + .proof + .commitments + .iter() + .filter(|commitment| commitment.is_quotient()) + .count(); + if quotient_count != self.num_quotients { + return Err(format!( + "quotient commitment count mismatch: plan={quotient_count} meta={}", + self.num_quotients + )); + } + + let grouped_commitments = self.commitment_read_groups().iter().sum::(); + if grouped_commitments != self.proof.commitments.len() { + return Err(format!( + "commitment group schedule mismatch: groups={grouped_commitments} commitments={}", + self.proof.commitments.len() + )); + } + + let non_quotient_count = self.num_non_quotient_commitments(); + let expected_non_quotient = self.advice_indices.len() + + self.num_lookups + + self.num_permutation_zs + + self.lookup_chunks.iter().sum::() + + self.num_lookups + + self.num_trashcans; + if non_quotient_count != expected_non_quotient { + return Err(format!( + "non-quotient commitment count mismatch: plan={non_quotient_count} expected={expected_non_quotient}" + )); + } + + if self + .proof + .evals + .iter() + .any(|eval| matches!(eval, EvalRead::Fixed(q) if self.simple_selector_cols.contains(&q.column))) + { + return Err("simple selector fixed column appears in proof eval reads".to_string()); + } + + let expected_perm_z_evals = if self.num_permutation_zs == 0 { + 0 + } else { + 3 * self.num_permutation_zs - 1 + }; + let actual_perm_z_evals = self + .proof + .evals + .iter() + .filter(|eval| matches!(eval, EvalRead::PermutationZ { .. })) + .count(); + if actual_perm_z_evals != expected_perm_z_evals { + return Err(format!( + "permutation z eval count mismatch: plan={actual_perm_z_evals} expected={expected_perm_z_evals}" + )); + } + + let expected_lookup_evals = self.lookup_chunks.iter().sum::() + 3 * self.num_lookups; + let actual_lookup_evals = self + .proof + .evals + .iter() + .filter(|eval| { + matches!( + eval, + EvalRead::LookupMultiplicity { .. } + | EvalRead::LookupHelper { .. } + | EvalRead::LookupAccumulator { .. } + ) + }) + .count(); + if actual_lookup_evals != expected_lookup_evals { + return Err(format!( + "lookup eval count mismatch: plan={actual_lookup_evals} expected={expected_lookup_evals}" + )); + } + + if !matches!(self.pcs_queries.last(), Some(PcsQuerySource::Linearization)) { + return Err("PCS query schedule must end with linearization query".to_string()); + } + + // Every proof G1 commitment absorbed into Fiat-Shamir must either be + // opened by PCS or consumed by a generated EIP-2537 MSM/pairing path. + // Advice commitments are the only category whose read set can be + // wider than the opened query set for a malformed/unsupported circuit. + // Reject those plans at codegen time rather than paying a precompile + // validation call for every absorbed proof point. + let opened_advice_cols = + self.advice_queries.iter().map(|query| query.column).collect::>(); + for column in 0..self.advice_indices.len() { + if !opened_advice_cols.contains(&column) { + return Err(format!( + "advice commitment column {column} is absorbed but never opened by PCS" + )); + } + } + + let pcs_lookup_multiplicities = self + .pcs_queries + .iter() + .filter(|query| matches!(query, PcsQuerySource::LookupMultiplicity { .. })) + .count(); + if pcs_lookup_multiplicities != self.num_lookups { + return Err(format!( + "lookup multiplicity PCS coverage mismatch: pcs={pcs_lookup_multiplicities} expected={}", + self.num_lookups + )); + } + + let pcs_lookup_helpers = self + .pcs_queries + .iter() + .filter(|query| matches!(query, PcsQuerySource::LookupHelper { .. })) + .count(); + let expected_lookup_helpers = self.lookup_chunks.iter().sum::(); + if pcs_lookup_helpers != expected_lookup_helpers { + return Err(format!( + "lookup helper PCS coverage mismatch: pcs={pcs_lookup_helpers} expected={expected_lookup_helpers}" + )); + } + + let pcs_lookup_accumulators = self + .pcs_queries + .iter() + .filter_map(|query| match query { + PcsQuerySource::LookupAccumulator { lookup, .. } => Some(*lookup), + _ => None, + }) + .collect::>(); + if pcs_lookup_accumulators.len() != self.num_lookups { + return Err(format!( + "lookup accumulator PCS coverage mismatch: pcs={} expected={}", + pcs_lookup_accumulators.len(), + self.num_lookups + )); + } + + let pcs_permutation_sets = self + .pcs_queries + .iter() + .filter_map(|query| match query { + PcsQuerySource::PermutationZ { set, .. } => Some(*set), + _ => None, + }) + .collect::>(); + if pcs_permutation_sets.len() != self.num_permutation_zs { + return Err(format!( + "permutation product PCS coverage mismatch: pcs={} expected={}", + pcs_permutation_sets.len(), + self.num_permutation_zs + )); + } + + let pcs_trash = self + .pcs_queries + .iter() + .filter(|query| matches!(query, PcsQuerySource::Trash { .. })) + .count(); + if pcs_trash != self.num_trashcans { + return Err(format!( + "trashcan PCS coverage mismatch: pcs={pcs_trash} expected={}", + self.num_trashcans + )); + } + + if self.quotient_trace_ids.len() != self.quotient.total() { + return Err(format!( + "quotient trace id count mismatch: ids={} identities={}", + self.quotient_trace_ids.len(), + self.quotient.total() + )); + } + let lookup_identity_count = + self.lookup_chunks.iter().map(|chunks| chunks + 2).sum::(); + if self.quotient.lookup != lookup_identity_count { + return Err(format!( + "lookup quotient identity count mismatch: plan={} expected={lookup_identity_count}", + self.quotient.lookup + )); + } + if self.pcs_query_trace_ids.len() != self.pcs_queries.len() { + return Err(format!( + "PCS trace id count mismatch: ids={} queries={}", + self.pcs_query_trace_ids.len(), + self.pcs_queries.len() + )); + } + if self.quotient_trace_ids.iter().any(|id| self.pcs_query_trace_ids.contains(id)) { + return Err("quotient and PCS trace id spaces overlap".to_string()); + } + + let pcs_without_linearization = self.pcs_queries.len().saturating_sub(1); + let proof_query_evals = self.proof.evals.len(); + if pcs_without_linearization != proof_query_evals { + return Err(format!( + "PCS/proof eval query mismatch before linearization: pcs={pcs_without_linearization} evals={proof_query_evals}" + )); + } + + let committed_instance_reads = self.proof.evals.iter().filter_map(|eval| match eval { + EvalRead::CommittedInstance(q) => Some(q), + _ => None, + }); + for q in committed_instance_reads { + if q.column >= self.num_committed_instances { + return Err(format!( + "non-committed instance column {} is read from proof", + q.column + )); + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use midnight_proofs::{ + plonk::{Constraints, FirstPhase, SecondPhase}, + poly::Rotation, + }; + use proptest::prelude::*; + + use super::*; + + /// Small two-advice constraint system used by protocol-plan property tests. + fn simple_cs() -> ConstraintSystem { + let mut cs = ConstraintSystem::default(); + let a0 = cs.advice_column(); + let a1 = cs.advice_column(); + let fixed = cs.fixed_column(); + let instance = cs.instance_column(); + cs.create_gate("mul with fixed and instance", |meta| { + let a0 = meta.query_advice(a0, Rotation::cur()); + let a1_next = meta.query_advice(a1, Rotation::next()); + let fixed_prev = meta.query_fixed(fixed, Rotation::prev()); + let instance_cur = meta.query_instance(instance, Rotation::cur()); + Constraints::without_selector(vec![("basic", a0 * a1_next + fixed_prev + instance_cur)]) + }); + cs + } + + #[test] + fn expression_visitor_collects_queries() { + let mut cs = ConstraintSystem::default(); + let advice = cs.advice_column(); + let fixed = cs.fixed_column(); + let instance = cs.instance_column(); + let challenge = cs.challenge_usable_after(FirstPhase); + cs.create_gate("visitor", |meta| { + let advice = meta.query_advice(advice, Rotation::next()); + let fixed = meta.query_fixed(fixed, Rotation::prev()); + let instance = meta.query_instance(instance, Rotation::cur()); + let challenge = meta.query_challenge(challenge); + Constraints::without_selector(vec![("visitor", advice + fixed * instance + challenge)]) + }); + let expr = cs.gates()[0].polynomials()[0].clone(); + let used = used_query(&expr); + assert!(used.advice.contains(&QueryKey::new(advice.index(), 1))); + assert!(used.fixed.contains(&QueryKey::new(fixed.index(), -1))); + assert!(used.instance.contains(&QueryKey::new(instance.index(), 0))); + assert!(used.challenges.contains(&challenge.index())); + } + + #[test] + fn plan_preserves_eval_and_pcs_order_for_basic_cs() { + let cs = simple_cs(); + let plan = ProtocolPlan::from_constraint_system(&cs, 1); + + assert_eq!( + plan.advice_queries.iter().map(|q| q.tuple()).collect_vec(), + vec![(0, 0), (1, 1)] + ); + assert_eq!( + plan.instance_queries.iter().map(|q| q.tuple()).collect_vec(), + vec![(0, 0)] + ); + assert_eq!( + &plan.proof.evals[..], + &[ + EvalRead::CommittedInstance(QueryKey::new(0, 0)), + EvalRead::Advice(QueryKey::new(0, 0)), + EvalRead::Advice(QueryKey::new(1, 1)), + EvalRead::Fixed(QueryKey::new(0, -1)), + ] + ); + assert_eq!( + &plan.pcs_queries[..], + &[ + PcsQuerySource::Advice(QueryKey::new(0, 0)), + PcsQuerySource::Advice(QueryKey::new(1, 1)), + PcsQuerySource::CommittedInstance(QueryKey::new(0, 0)), + PcsQuerySource::Fixed(QueryKey::new(0, -1)), + PcsQuerySource::Linearization, + ] + ); + assert_eq!(plan.num_main_evals(), 4); + assert_eq!(plan.commitment_read_groups(), vec![2, plan.num_quotients]); + assert_eq!(plan.quotient_trace_ids, vec![TRACE_QUOTIENT_IDENTITY_BASE]); + assert_eq!( + plan.pcs_query_trace_ids, + (0..plan.pcs_queries.len()) + .map(|idx| TRACE_PCS_QUERY_BASE + idx as u64) + .collect::>() + ); + assert!(plan.validate().is_ok()); + } + + #[test] + fn plan_skips_non_committed_instance_eval_reads() { + let cs = simple_cs(); + let plan = ProtocolPlan::from_constraint_system(&cs, 0); + assert!(!plan + .proof + .evals + .iter() + .any(|eval| matches!(eval, EvalRead::CommittedInstance(_)))); + assert!(!plan + .pcs_queries + .iter() + .any(|query| matches!(query, PcsQuerySource::CommittedInstance(_)))); + } + + #[test] + fn plan_tracks_permutation_chunking() { + let mut cs = ConstraintSystem::default(); + let a0 = cs.advice_column(); + let a1 = cs.advice_column(); + let a2 = cs.advice_column(); + let fixed = cs.fixed_column(); + let instance = cs.instance_column(); + cs.enable_equality(a0); + cs.enable_equality(fixed); + cs.enable_equality(instance); + cs.create_gate("degree three", |meta| { + let a0 = meta.query_advice(a0, Rotation::cur()); + let a1 = meta.query_advice(a1, Rotation::cur()); + let a2 = meta.query_advice(a2, Rotation::cur()); + Constraints::without_selector(vec![("degree three", a0 * a1 * a2)]) + }); + + let plan = ProtocolPlan::from_constraint_system(&cs, 0); + assert_eq!(plan.permutation_columns.len(), 3); + assert!(plan.num_permutation_zs >= 1); + assert_eq!( + plan.proof + .evals + .iter() + .filter(|eval| matches!(eval, EvalRead::PermutationZ { .. })) + .count(), + 3 * plan.num_permutation_zs - 1 + ); + assert!(plan.validate().is_ok()); + } + + #[test] + fn lookup_identity_source_handles_variable_chunk_counts() { + let lookup_chunks = vec![1usize, 4, 2]; + let lookup_count = lookup_chunks.iter().map(|chunks| chunks + 2).sum(); + let plan = ProtocolPlan { + lookup_chunks, + quotient: QuotientIdentityPlan { + lookup: lookup_count, + ..QuotientIdentityPlan::default() + }, + ..ProtocolPlan::default() + }; + + assert_eq!(plan.lookup_identity_source(0), Some((0, 0))); + assert_eq!(plan.lookup_identity_source(2), Some((0, 2))); + assert_eq!(plan.lookup_identity_source(3), Some((1, 0))); + assert_eq!(plan.lookup_identity_source(8), Some((1, 5))); + assert_eq!(plan.lookup_identity_source(9), Some((2, 0))); + assert_eq!(plan.lookup_identity_source(12), Some((2, 3))); + assert_eq!(plan.lookup_identity_source(13), None); + } + + #[test] + fn plan_allows_challenge_phase_beyond_advice_phases() { + let mut cs = ConstraintSystem::default(); + let advice_first = cs.advice_column(); + let advice_second = cs.advice_column_in(SecondPhase); + let challenge = cs.challenge_usable_after(SecondPhase); + cs.create_gate("late challenge", |meta| { + let advice_first = meta.query_advice(advice_first, Rotation::cur()); + let advice_second = meta.query_advice(advice_second, Rotation::cur()); + let challenge = meta.query_challenge(challenge); + Constraints::without_selector(vec![( + "late challenge", + advice_first + advice_second + challenge, + )]) + }); + + let plan = ProtocolPlan::from_constraint_system(&cs, 0); + assert_eq!(plan.num_user_advices[0], 1); + assert_eq!(plan.num_user_advices[1], 1); + assert_eq!(plan.num_user_challenges.iter().sum::(), 1); + assert_eq!(plan.challenge_indices.len(), 1); + assert!(plan.validate().is_ok()); + } + + #[test] + fn quotient_commitment_count_matches_outer_single_h_feature() { + let mut cs = ConstraintSystem::default(); + let a0 = cs.advice_column(); + let a1 = cs.advice_column(); + let a2 = cs.advice_column(); + cs.create_gate("degree three", |meta| { + let a0 = meta.query_advice(a0, Rotation::cur()); + let a1 = meta.query_advice(a1, Rotation::cur()); + let a2 = meta.query_advice(a2, Rotation::cur()); + Constraints::without_selector(vec![("degree three", a0 * a1 * a2)]) + }); + + let plan = ProtocolPlan::from_constraint_system(&cs, 0); + let expected = if crate::OUTER_SINGLE_H_COMMITMENT_ENABLED { + 1 + } else { + cs.degree() - 1 + }; + assert_eq!(plan.num_quotients, expected); + assert_eq!( + plan.proof.commitments.iter().filter(|read| read.is_quotient()).count(), + expected + ); + } + + #[test] + fn validation_rejects_simple_selector_eval_reads() { + let mut plan = ProtocolPlan::from_constraint_system(&simple_cs(), 0); + plan.simple_selector_cols.insert(0); + plan.proof.evals.push(EvalRead::Fixed(QueryKey::new(0, 0))); + let err = plan.validate().unwrap_err(); + assert!(err.contains("simple selector")); + } + + #[test] + fn validation_rejects_absorbed_unopened_advice_commitments() { + let mut plan = ProtocolPlan::from_constraint_system(&simple_cs(), 0); + plan.advice_queries.retain(|query| query.column != 1); + plan.proof + .evals + .retain(|eval| !matches!(eval, EvalRead::Advice(query) if query.column == 1)); + plan.pcs_queries + .retain(|query| !matches!(query, PcsQuerySource::Advice(query) if query.column == 1)); + + let err = plan.validate().unwrap_err(); + assert!( + err.contains("absorbed but never opened by PCS"), + "unexpected validation error: {err}" + ); + } + + proptest! { + #[test] + fn protocol_plan_invariants_hold_for_small_constraint_systems( + n_advice in 1usize..6, + n_fixed in 0usize..4, + n_instance in 0usize..3, + n_perm in 0usize..6, + n_committed_instance in 0usize..3, + ) { + let mut cs = ConstraintSystem::default(); + let advice = (0..n_advice).map(|_| cs.advice_column()).collect::>(); + let fixed = (0..n_fixed).map(|_| cs.fixed_column()).collect::>(); + let instance = (0..n_instance).map(|_| cs.instance_column()).collect::>(); + + for column in advice.iter().take(n_perm.min(n_advice)) { + cs.enable_equality(*column); + } + + cs.create_gate("pbt protocol plan", |meta| { + let mut expr = meta.query_advice(advice[0], Rotation::cur()) + * meta.query_advice(advice[0], Rotation::next()) + * meta.query_advice(advice[0], Rotation::prev()); + + for (idx, column) in advice.iter().enumerate() { + let rotation = match idx % 3 { + 0 => Rotation::cur(), + 1 => Rotation::next(), + _ => Rotation::prev(), + }; + expr = expr + meta.query_advice(*column, rotation); + } + for (idx, column) in fixed.iter().enumerate() { + let rotation = if idx % 2 == 0 { Rotation::cur() } else { Rotation::prev() }; + expr = expr + meta.query_fixed(*column, rotation); + } + for (idx, column) in instance.iter().enumerate() { + let rotation = if idx % 2 == 0 { Rotation::cur() } else { Rotation::next() }; + expr = expr + meta.query_instance(*column, rotation); + } + + Constraints::without_selector(vec![("pbt", expr)]) + }); + + let committed = n_committed_instance.min(n_instance); + let plan = ProtocolPlan::from_constraint_system(&cs, committed); + prop_assert!(plan.validate().is_ok()); + prop_assert_eq!(plan.pcs_queries.len(), plan.proof.evals.len() + 1); + prop_assert_eq!( + plan.commitment_read_groups().iter().sum::(), + plan.proof.commitments.len() + ); + prop_assert_eq!(plan.quotient_trace_ids.len(), plan.quotient.total()); + prop_assert_eq!(plan.pcs_query_trace_ids.len(), plan.pcs_queries.len()); + let simple_selector_eval = plan + .proof + .evals + .iter() + .all(|eval| !matches!(eval, EvalRead::Fixed(q) if plan.simple_selector_cols.contains(&q.column))); + prop_assert!(simple_selector_eval); + + let committed_instance_reads_in_bounds = plan + .proof + .evals + .iter() + .filter_map(|eval| match eval { + EvalRead::CommittedInstance(q) => Some(q.column), + _ => None, + }) + .all(|column| column < committed); + prop_assert!(committed_instance_reads_in_bounds); + } + } +} diff --git a/proofs/solidity-verifier/src/lowering/quotient.rs b/proofs/solidity-verifier/src/lowering/quotient.rs new file mode 100644 index 000000000..d77a5e504 --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/quotient.rs @@ -0,0 +1,1927 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Quotient planning and lowering for a concrete verifier build. +//! +//! These methods build compact quotient VM artifacts, native callback choices, +//! selector folds, and execution manifests for the concrete verifying key bound +//! to the generator. + +use std::collections::{HashMap, HashSet}; + +use ff::{Field, PrimeField}; +use midnight_curves::Fq; +use midnight_proofs::plonk::Expression; + +use crate::{ + api::QuotientIdentitySource, + lowering::{ + config, + encoding::{fe_to_u256, ConstraintSystemMeta, Data, Ptr}, + layout::{ + memory::{VerifierMemoryLayout, WORD_BYTES}, + vk_payload::PackedProgramCodec, + }, + quotient_numerator::{vm::*, Evaluator}, + render::{ + Halo2VerifyingKey, QuotientExternal, QuotientProgram, QuotientSelectorTail, + QuotientVmMemUsage, QuotientVmOpcodeUsage, + }, + VerifierBuildInputs, + }, +}; + +/// Persistent quotient VM state words before optional selector-power storage. +pub(crate) const QUOTIENT_FIXED_STATE_WORDS: usize = 2; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct QuotientStateSlots { + // Persistent VM state words shared by inline prefixes, bytecode identities, + // and native callbacks so they advance the same y-batch. + pub(crate) eval_numer_mptr: usize, + pub(crate) trace_id_mptr: usize, + pub(crate) selector_power_mptr: usize, +} + +impl QuotientStateSlots { + /// Place persistent VM state at the start of the quotient state region. + fn new(state_mptr: usize) -> Self { + // Layout at `quotient_tmp_mptr`: + // [0 .. +2 words) accumulator and trace-id state + // [after state] optional y^k selector power table + Self { + eval_numer_mptr: state_mptr, + trace_id_mptr: state_mptr + WORD_BYTES, + selector_power_mptr: state_mptr + 2 * WORD_BYTES, + } + } +} + +#[derive(Clone, Debug)] +pub(crate) struct NativeGateCandidate { + pub(crate) gate_idx: usize, + pub(crate) vm_bytes: usize, + pub(crate) native_bytes: usize, + pub(crate) gas_saved: u64, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub(crate) struct NativeGateSelectionState { + pub(crate) gas_saved: u64, + pub(crate) native_bytes: usize, + pub(crate) gate_indices: Vec, +} + +#[derive(Clone, Debug, Default)] +pub(crate) struct QuotientComputationBlocks { + pub(crate) inline_computations: Vec>, + pub(crate) eval_numer_computations: Vec>, + pub(crate) post_vm_computations: Vec>, + pub(crate) native_permutation_computation: Vec, + pub(crate) native_lookup_computation: Vec, + pub(crate) native_identity_computations: Vec>, +} + +#[derive(Clone, Copy, Debug, Default)] +pub(crate) struct QuotientHelperFlags { + pub(crate) pow5: bool, + pub(crate) limb7: bool, + pub(crate) wide_limb7: bool, +} + +impl QuotientComputationBlocks { + /// Scan generated Yul blocks for optional helper calls the template must + /// include. + pub(crate) fn helper_flags(&self) -> QuotientHelperFlags { + QuotientHelperFlags { + pow5: self.any_line_contains("q_pow5("), + limb7: self.any_line_contains("q_limb7("), + wide_limb7: self.any_line_contains("q_limb7_wide("), + } + } + + /// Return true if any emitted quotient block references `needle`. + fn any_line_contains(&self, needle: &str) -> bool { + self.inline_computations.iter().any(|block| block_contains(block, needle)) + || self.eval_numer_computations.iter().any(|block| block_contains(block, needle)) + || self.post_vm_computations.iter().any(|block| block_contains(block, needle)) + || block_contains(&self.native_permutation_computation, needle) + || block_contains(&self.native_lookup_computation, needle) + || self + .native_identity_computations + .iter() + .any(|block| block_contains(block, needle)) + } +} + +/// Return true if any generated Yul line in `block` contains `needle`. +fn block_contains(block: &[String], needle: &str) -> bool { + block.iter().any(|line| line.contains(needle)) +} + +impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { + /// Build the compact quotient VM artifact for a metadata/data snapshot. + pub(super) fn compact_quotient_program_for( + &self, + meta: &ConstraintSystemMeta, + data: &Data, + ) -> (QuotientProgramBuild, Vec) { + // Build the exact compact program carried by the VK. This helper is + // also used during static-layout convergence, so its output must be + // deterministic for a fixed VK base and proof shape. + let plan = self.quotient_program_plan(meta, data); + let sorted_simple = plan.sorted_simple.clone(); + let quotient_program_build = + self.build_quotient_program_items(&plan.items, &plan.selector_fold); + let _quotient_max_stack = quotient_program_build.max_stack; + (quotient_program_build, sorted_simple) + } + + /// Convert a finalized VM build into the template's VK-backed program + /// model. + /// + /// This is the single handoff point between the quotient bytecode compiler, + /// VK payload reservation, and the generated Yul interpreter. Keeping the + /// bounds checks here prevents the standalone evaluator and in-verifier VM + /// paths from drifting. + pub(super) fn quotient_template_program( + &self, + build: QuotientProgramBuild, + vk: &Halo2VerifyingKey, + vk_mptr: Ptr, + memory: &VerifierMemoryLayout, + selector_fold: &SelectorFoldPlan, + ) -> (QuotientProgram, usize, QuotientStateSlots) { + let quotient_program_chunks = PackedProgramCodec::encode_words(&build.bytes); + let quotient_const_words = vk.quotient_const_words; + let quotient_program_words = vk.quotient_program_words; + let quotient_const_offset_words = + vk.quotient_const_offset_words.expect("VK must carry quotient constants"); + let quotient_program_offset_words = + vk.quotient_program_offset_words.expect("VK must carry quotient program"); + assert!( + build.consts.len() <= quotient_const_words, + "quotient const table exceeded VK payload reservation" + ); + assert!( + quotient_program_chunks.len() <= quotient_program_words, + "quotient program length exceeded VK payload reservation" + ); + + let quotient_stack_mptr = memory.quotient_stack_mptr; + let const_mptr = (vk_mptr + quotient_const_offset_words).value().as_usize(); + let program_mptr = (vk_mptr + quotient_program_offset_words).value().as_usize(); + let state_slots = QuotientStateSlots::new(memory.quotient_tmp_mptr); + let len = build.bytes.len(); + let op_usage = Self::quotient_opcode_usage(&build.used_ops); + let mem_usage = Self::quotient_mem_usage(&build.used_mem_tokens); + let program = QuotientProgram { + len, + op_usage, + mem_usage, + const_mptr, + eval_numer_mptr: state_slots.eval_numer_mptr, + trace_id_mptr: state_slots.trace_id_mptr, + selector_power_mptr: state_slots.selector_power_mptr, + selector_max_power: selector_fold.max_power, + selector_tail_updates: Self::selector_tail_updates(selector_fold), + stack_mptr: quotient_stack_mptr, + program_mptr, + }; + + (program, quotient_stack_mptr, state_slots) + } + + /// Convert finalized bytecode usage into template switch-arm flags. + fn quotient_opcode_usage(used_ops: &[u8]) -> QuotientVmOpcodeUsage { + let has = |op| used_ops.contains(&op); + QuotientVmOpcodeUsage { + push_const: has(Q_OP_PUSH_CONST), + push_mem_literal: has(Q_OP_PUSH_MEM_LITERAL), + push_mem_token: has(Q_OP_PUSH_MEM_TOKEN), + push_mem_token_offset: has(Q_OP_PUSH_MEM_TOKEN_OFFSET), + push_mem_u16: has(Q_OP_PUSH_MEM_U16), + add: has(Q_OP_ADD), + mul: has(Q_OP_MUL), + neg: has(Q_OP_NEG), + push_const_u8: has(Q_OP_PUSH_CONST_U8), + fold_main: has(Q_OP_FOLD_MAIN), + fold_selector: has(Q_OP_FOLD_SELECTOR), + add_const_u8: has(Q_OP_ADD_CONST_U8), + mul_const_u8: has(Q_OP_MUL_CONST_U8), + add_const: has(Q_OP_ADD_CONST), + mul_const: has(Q_OP_MUL_CONST), + add_mem_u16: has(Q_OP_ADD_MEM_U16), + mul_mem_u16: has(Q_OP_MUL_MEM_U16), + add_mul_mem_mem_const_u8: has(Q_OP_ADD_MUL_MEM_MEM_CONST_U8), + add_mul_const_u8_mem_u16: has(Q_OP_ADD_MUL_CONST_U8_MEM_U16), + add_mul_mem_mem: has(Q_OP_ADD_MUL_MEM_MEM), + run_add_mul_mem_mem_const_u8: has(Q_OP_RUN_ADD_MUL_MEM_MEM_CONST_U8), + run_add_mul_const_u8_mem_u16: has(Q_OP_RUN_ADD_MUL_CONST_U8_MEM_U16), + affine_sum: has(Q_OP_AFFINE_SUM), + native_permutation: has(Q_OP_NATIVE_PERMUTATION), + native_lookup: has(Q_OP_NATIVE_LOOKUP), + native_identity: has(Q_OP_NATIVE_IDENTITY), + lin7: has(Q_OP_LIN7), + bilin7_row: has(Q_OP_BILIN7_ROW), + bilin7_pairwise: has(Q_OP_BILIN7_PAIRWISE), + modarith7: has(Q_OP_MODARITH7), + pow5: has(Q_OP_POW5), + } + } + + /// Convert finalized memory-token usage into template switch-arm flags. + fn quotient_mem_usage(used_tokens: &[u8]) -> QuotientVmMemUsage { + let has = |token| used_tokens.contains(&token); + QuotientVmMemUsage { + l0: has(Q_MEM_L0), + l_last: has(Q_MEM_L_LAST), + l_blind: has(Q_MEM_L_BLIND), + beta: has(Q_MEM_BETA), + gamma: has(Q_MEM_GAMMA), + x: has(Q_MEM_X), + theta: has(Q_MEM_THETA), + trash_challenge: has(Q_MEM_TRASH_CHALLENGE), + instance_eval: has(Q_MEM_INSTANCE_EVAL), + } + } + + /// Return stack/scratch words needed by interpreted VM and native + /// callbacks. + pub(crate) fn quotient_stack_words_for_build( + build: &QuotientProgramBuild, + native_callback_scratch_words: usize, + ) -> usize { + // `build.max_stack` only describes the interpreted operand stack. Some + // native callbacks share `quotient_stack_mptr` as a scratch base, so + // the registered memory region must cover both possible users. + build.max_stack.max(native_callback_scratch_words) + } + + /// Number of persistent VM temp words needed for state plus selector + /// powers. + pub(super) fn quotient_state_words(selector_fold: &SelectorFoldPlan) -> usize { + QUOTIENT_FIXED_STATE_WORDS + Self::selector_power_words(selector_fold) + } + + /// Number of `y^k` words needed by selector gap/tail folding. + fn selector_power_words(selector_fold: &SelectorFoldPlan) -> usize { + if selector_fold.max_power == 0 { + 0 + } else { + selector_fold.max_power + 1 + } + } + + /// Render-time selector tail updates, omitting zero tails. + fn selector_tail_updates(selector_fold: &SelectorFoldPlan) -> Vec { + selector_fold + .tail_exponents + .iter() + .enumerate() + .filter_map(|(selector_idx, tail)| { + (*tail != 0).then_some(QuotientSelectorTail { + selector_offset: selector_idx * WORD_BYTES, + power_offset: tail * WORD_BYTES, + }) + }) + .collect() + } + + /// Build the codegen-time selector gap schedule over the full identity + /// stream. + pub(crate) fn selector_fold_plan( + identities: &[QuotientIdentity], + selector_count: usize, + ) -> SelectorFoldPlan { + let selector_positions = (0..selector_count) + .map(|selector_idx| { + identities + .iter() + .filter_map(|identity| match identity.target { + QuotientTarget::Selector(idx) if idx == selector_idx => { + Some(identity.meta.global_index) + } + QuotientTarget::Selector(_) | QuotientTarget::Main => None, + }) + .collect::>() + }) + .collect::>(); + let target_by_global_index = identities + .iter() + .map(|identity| (identity.meta.global_index, identity.target)) + .collect::>(); + let selector_gap_by_global_index = selector_positions + .iter() + .flat_map(|positions| { + positions.iter().enumerate().map(|(pos, &global_index)| { + let gap = + pos.checked_sub(1).map_or(0, |prev_pos| global_index - positions[prev_pos]); + (global_index, gap) + }) + }) + .collect::>(); + + let gaps_by_identity = (0..identities.len()) + .map(|global_index| { + match target_by_global_index + .get(&global_index) + .copied() + .expect("quotient identity global indices must be dense") + { + QuotientTarget::Selector(_) => Some( + *selector_gap_by_global_index + .get(&global_index) + .expect("selector identity must be present in selector gaps"), + ), + QuotientTarget::Main => None, + } + }) + .collect::>(); + let tail_exponents = selector_positions + .iter() + .map(|positions| { + positions.last().map_or(0, |last_index| identities.len() - 1 - last_index) + }) + .collect::>(); + let max_power = gaps_by_identity + .iter() + .flatten() + .chain(tail_exponents.iter()) + .copied() + .max() + .unwrap_or(0); + + SelectorFoldPlan { + gaps_by_identity, + tail_exponents, + max_power, + } + } + + /// Choose inline, VM, and native-callback representation for identities. + pub(super) fn quotient_program_plan( + &self, + meta: &ConstraintSystemMeta, + data: &Data, + ) -> QuotientProgramPlan { + // Preserve the Rust identity order while choosing an execution form for + // each identity: + // * a small gate prefix can stay inline, + // * ordinary identities become compact VM bytecode, + // * recognized expensive gates plus regular permutation/lookup families + // become native callback markers in the same stream. + let parts = self.quotient_identity_parts(meta, data); + let all_identities = parts.all_identities(); + let selector_fold = Self::selector_fold_plan(&all_identities, parts.sorted_simple.len()); + let inline_count = parts.gates.len().min(config::DEFAULT_HYBRID_QUOTIENT_INLINE_IDENTITIES); + let inline_identities = parts.gates[..inline_count].to_vec(); + let remaining_gates = &parts.gates[inline_count..]; + let native_gate_indices = Self::native_gate_indices(remaining_gates); + let native_permutation = meta.num_permutation_zs > 0; + let native_lookup = meta.num_lookups > 0; + let structured_trash_tail = meta.num_trashcans > 0; + + let native_identities = remaining_gates + .iter() + .enumerate() + .filter(|(gate_idx, _)| native_gate_indices.contains(gate_idx)) + .map(|(_, identity)| identity.clone()) + .collect::>(); + let native_index_by_gate = remaining_gates + .iter() + .enumerate() + .filter(|(gate_idx, _)| native_gate_indices.contains(gate_idx)) + .enumerate() + .map(|(native_idx, (gate_idx, _))| (gate_idx, native_idx)) + .collect::>(); + + let gate_items = remaining_gates.iter().enumerate().map(|(gate_idx, identity)| { + native_index_by_gate + .get(&gate_idx) + .copied() + .map(QuotientProgramItem::NativeIdentity) + .unwrap_or_else(|| QuotientProgramItem::Identity(identity.clone())) + }); + let permutation_items = if native_permutation { + vec![QuotientProgramItem::NativePermutation] + } else { + parts.permutation.iter().cloned().map(QuotientProgramItem::Identity).collect() + }; + let lookup_items = if native_lookup { + vec![QuotientProgramItem::NativeLookup] + } else { + parts.lookup.iter().cloned().map(QuotientProgramItem::Identity).collect() + }; + let trash_items = if structured_trash_tail { + Vec::new() + } else { + parts + .trash + .iter() + .cloned() + .map(QuotientProgramItem::Identity) + .collect::>() + }; + let items = gate_items + .chain(permutation_items) + .chain(lookup_items) + .chain(trash_items) + .collect(); + + let native_permutation_identities = if native_permutation { + parts.permutation.clone() + } else { + Vec::new() + }; + let native_lookup_identities = if native_lookup { + parts.lookup.clone() + } else { + Vec::new() + }; + let structured_tail_identities = if structured_trash_tail { + parts.trash.clone() + } else { + Vec::new() + }; + + let plan = QuotientProgramPlan { + inline_identities, + items, + native_identities, + native_permutation_identities, + native_lookup_identities, + structured_tail_identities, + sorted_simple: parts.sorted_simple, + has_native_permutation: native_permutation, + has_native_lookup: native_lookup, + selector_fold, + }; + plan.validate_execution_manifest(&all_identities) + .expect("quotient execution plan must preserve the identity stream"); + plan + } + + /// Pick native gate callbacks by estimated gas saved under a byte budget. + fn native_gate_indices(gates: &[QuotientIdentity]) -> HashSet { + let max_count = gates.len().min(config::DEFAULT_QUOTIENT_NATIVE_GATES); + if max_count == 0 { + return HashSet::new(); + } + + let candidates = Self::native_gate_candidates(gates); + let byte_budget = Self::default_native_gate_byte_budget(&candidates, max_count); + let selection = Self::select_native_gate_candidates(&candidates, max_count, byte_budget); + + selection.gate_indices.into_iter().collect() + } + + /// Build per-gate native-callback selection metrics. + fn native_gate_candidates(gates: &[QuotientIdentity]) -> Vec { + gates + .iter() + .enumerate() + .map(|(gate_idx, identity)| { + let (vm_bytes, vm_gas) = Self::quotient_identity_program_metrics(identity); + let native_block = Self::native_identity_estimate_block(identity); + let native_bytes = Self::yul_block_source_bytes(&native_block); + let native_gas = Self::estimate_native_yul_gas(&native_block); + NativeGateCandidate { + gate_idx, + vm_bytes, + native_bytes, + gas_saved: vm_gas.saturating_sub(native_gas), + } + }) + .collect() + } + + /// Preserve the previous top-N byte envelope as the default native budget. + pub(crate) fn default_native_gate_byte_budget( + candidates: &[NativeGateCandidate], + max_count: usize, + ) -> usize { + let mut ranked = candidates + .iter() + .map(|candidate| { + ( + candidate.vm_bytes, + candidate.gate_idx, + candidate.native_bytes, + ) + }) + .collect::>(); + ranked.sort_by(|(lhs_bytes, lhs_idx, _), (rhs_bytes, rhs_idx, _)| { + rhs_bytes.cmp(lhs_bytes).then_with(|| lhs_idx.cmp(rhs_idx)) + }); + ranked + .into_iter() + .take(max_count) + .map(|(_, _, native_bytes)| native_bytes) + .sum() + } + + /// Solve a small 0/1 knapsack: maximize estimated gas saved under bytes. + pub(crate) fn select_native_gate_candidates( + candidates: &[NativeGateCandidate], + max_count: usize, + byte_budget: usize, + ) -> NativeGateSelectionState { + if max_count == 0 || byte_budget == 0 { + return NativeGateSelectionState::default(); + } + + // Round byte budgets to EVM-word units to keep the dynamic-programming + // table small while preserving the byte-budget ordering. + const BYTE_UNIT: usize = 32; + let budget_units = byte_budget.div_ceil(BYTE_UNIT); + let mut dp = vec![vec![None::; budget_units + 1]; max_count + 1]; + dp[0][0] = Some(NativeGateSelectionState::default()); + + for candidate in candidates + .iter() + .filter(|candidate| candidate.gas_saved > 0 && candidate.native_bytes > 0) + { + let cost_units = candidate.native_bytes.div_ceil(BYTE_UNIT).max(1); + if cost_units > budget_units { + continue; + } + + for count in (0..max_count).rev() { + for used in (0..=budget_units - cost_units).rev() { + let Some(state) = dp[count][used].clone() else { + continue; + }; + let mut next = state; + next.gas_saved += candidate.gas_saved; + next.native_bytes += candidate.native_bytes; + next.gate_indices.push(candidate.gate_idx); + + let next_count = count + 1; + let next_used = used + cost_units; + if Self::native_gate_selection_better(&next, dp[next_count][next_used].as_ref()) + { + dp[next_count][next_used] = Some(next); + } + } + } + } + + let mut best = NativeGateSelectionState::default(); + for count_states in dp { + for state in count_states.into_iter().flatten() { + if Self::native_gate_selection_better(&state, Some(&best)) { + best = state; + } + } + } + best + } + + /// Deterministic tie-breaker for native gate knapsack states. + fn native_gate_selection_better( + candidate: &NativeGateSelectionState, + incumbent: Option<&NativeGateSelectionState>, + ) -> bool { + let Some(incumbent) = incumbent else { + return true; + }; + candidate + .gas_saved + .cmp(&incumbent.gas_saved) + .then_with(|| incumbent.native_bytes.cmp(&candidate.native_bytes)) + .then_with(|| incumbent.gate_indices.len().cmp(&candidate.gate_indices.len())) + .then_with(|| incumbent.gate_indices.cmp(&candidate.gate_indices)) + == std::cmp::Ordering::Greater + } + + /// Estimate the generated native callback block for one identity. + fn native_identity_estimate_block(identity: &QuotientIdentity) -> Vec { + let state_slots = QuotientStateSlots { + eval_numer_mptr: 0x2000, + trace_id_mptr: 0x2020, + selector_power_mptr: 0x2040, + }; + let selector_gap = matches!(identity.target, QuotientTarget::Selector(_)).then_some(1); + Self::direct_quotient_block( + &identity.lines, + &identity.var, + identity.target, + selector_gap, + 0x1000, + state_slots, + false, + ) + } + + /// Source-byte proxy for the native callback's contribution to runtime + /// size. + fn yul_block_source_bytes(block: &[String]) -> usize { + block.iter().map(|line| line.len() + 1).sum() + } + + /// Relative gas proxy for a generated native Yul callback. + fn estimate_native_yul_gas(block: &[String]) -> u64 { + let raw = block + .iter() + .map(|line| { + let mut gas = 4u64; + gas += 8 * line.matches("mload(").count() as u64; + gas += 10 * line.matches("mstore(").count() as u64; + gas += 38 * line.matches("addmod(").count() as u64; + gas += 42 * line.matches("mulmod(").count() as u64; + gas += 18 * line.matches("add(").count() as u64; + gas += 18 * line.matches("sub(").count() as u64; + gas += 16 * line.matches("mul(").count() as u64; + gas + }) + .sum::(); + // This is a relative selector score, not an absolute EVM gas model: + // straight-line native Yul avoids the compact VM's dispatch overhead. + raw / 2 + } + + /// Estimate compact-VM byte and gas cost of one identity. + fn quotient_identity_program_metrics(identity: &QuotientIdentity) -> (usize, u64) { + let mut builder = QuotientProgramBuilder::default(); + let expr = Self::quotient_identity_expr(identity); + let selector_gap = matches!(identity.target, QuotientTarget::Selector(_)).then_some(0); + builder.identity_expr(&expr, identity.target, selector_gap); + let gas = Self::estimate_quotient_vm_gas(&builder.bytes); + (builder.bytes.len(), gas) + } + + /// Relative gas proxy for the compact quotient VM interpreter. + fn estimate_quotient_vm_gas(bytes: &[u8]) -> u64 { + quotient_bytecode_ops(bytes) + .map(|(idx, op, len)| match op { + Q_OP_PUSH_CONST => 54, + Q_OP_PUSH_MEM_LITERAL => 56, + Q_OP_PUSH_MEM_TOKEN => 60, + Q_OP_PUSH_MEM_TOKEN_OFFSET => 66, + Q_OP_PUSH_MEM_U16 => 48, + Q_OP_ADD => 38, + Q_OP_MUL => 42, + Q_OP_NEG => 24, + Q_OP_PUSH_CONST_U8 => 42, + Q_OP_FOLD_MAIN => 70, + Q_OP_FOLD_SELECTOR => 92, + Q_OP_ADD_CONST_U8 | Q_OP_ADD_CONST => 46, + Q_OP_MUL_CONST_U8 | Q_OP_MUL_CONST => 50, + Q_OP_ADD_MEM_U16 => 48, + Q_OP_MUL_MEM_U16 => 52, + Q_OP_ADD_MUL_MEM_MEM_CONST_U8 => 78, + Q_OP_ADD_MUL_CONST_U8_MEM_U16 => 66, + Q_OP_ADD_MUL_MEM_MEM => 70, + Q_OP_RUN_ADD_MUL_MEM_MEM_CONST_U8 => { + let count = read_u16(bytes, idx + 1) as u64; + 32 + 58 * count + } + Q_OP_RUN_ADD_MUL_CONST_U8_MEM_U16 => { + let count = read_u16(bytes, idx + 1) as u64; + 32 + 48 * count + } + Q_OP_NATIVE_PERMUTATION | Q_OP_NATIVE_LOOKUP => 0, + Q_OP_NATIVE_IDENTITY => 0, + Q_OP_LIN7 => 190, + Q_OP_BILIN7_ROW => 260, + Q_OP_BILIN7_PAIRWISE => 980, + Q_OP_MODARITH7 => 90 + 9 * quotient_op_len(bytes, idx) as u64, + Q_OP_POW5 => 58, + _ => len as u64 * 12, + }) + .sum() + } + + /// Compute the copied-memory frame required by the external evaluator. + pub(super) fn quotient_external_frame( + vk_mptr: Ptr, + vk_len: usize, + meta: &ConstraintSystemMeta, + memory: &VerifierMemoryLayout, + simple_selector_count: usize, + ) -> QuotientExternal { + let frame_base = vk_mptr.value().as_usize(); + Self::quotient_external_frame_from_bounds( + frame_base, + vk_len, + memory.reversed_evals_mptr.value().as_usize(), + meta.num_evals, + simple_selector_count, + ) + } + + /// Compute an external quotient frame from already-known range bounds. + pub(crate) fn quotient_external_frame_from_bounds( + frame_base: usize, + vk_len: usize, + evals_base: usize, + num_evals: usize, + simple_selector_count: usize, + ) -> QuotientExternal { + let vk_end = frame_base + vk_len; + let evals_end = evals_base + num_evals * WORD_BYTES; + let frame_end = vk_end.max(evals_end); + QuotientExternal { + frame_base, + frame_len: frame_end - frame_base, + output_len: 2 * WORD_BYTES + simple_selector_count * WORD_BYTES, + magic: QUOTIENT_EXTERNAL_MAGIC, + } + } + + /// Split the quotient identity stream into gate/permutation/lookup/trash + /// parts. + /// + /// Upstream reference: `plonk::partially_evaluate_identities` returns gate + /// identities first, then permutation, lookup, and trash identities, with + /// simple-selector gates tagged by their fixed-column index. This method + /// preserves that order and converts selector columns into local bucket + /// indices used by the generated linearization MSM. + pub(super) fn quotient_identity_parts( + &self, + meta: &ConstraintSystemMeta, + data: &Data, + ) -> QuotientIdentityParts { + // This is the codegen-time split of Midfall's + // `partially_evaluate_identities` comment: the Rust verifier returns + // `(Option, evaluation)` for gates first, then + // permutation, lookup, and trash identities. `Some(selector_column)` + // means the identity is gated by a simple multiplicative selector and + // must feed a selector bucket in `compute_linearization_commitment`; + // `None` means it is fully evaluated and contributes to the negated + // expected scalar. + let evaluator = Evaluator::new(self.vk.cs(), meta, data); + let gate_items = evaluator.gate_computations_tagged(); + let gate_exprs = self + .vk + .cs() + .gates() + .iter() + .flat_map(|gate| gate.polynomials().iter()) + .map(|poly| Self::quotient_expr_from_plonk_expr(meta, data, poly)) + .collect::>(); + assert_eq!( + gate_items.len(), + gate_exprs.len(), + "gate Yul expressions and typed expressions must stay aligned" + ); + let perm_items = evaluator.permutation_computations(); + let lookup_items = evaluator.lookup_computations(); + let trash_items = evaluator.trashcan_computations(); + + let mut sorted_simple: Vec = meta.simple_selector_cols.iter().copied().collect(); + sorted_simple.sort_unstable(); + + let gate_count = gate_items.len(); + let permutation_base = gate_count; + let lookup_base = permutation_base + perm_items.len(); + let trash_base = lookup_base + lookup_items.len(); + + let selector_target = |col| { + let idx = sorted_simple + .iter() + .position(|simple| *simple == col) + .expect("selector column present"); + QuotientTarget::Selector(idx) + }; + let gates = gate_items + .into_iter() + .zip(gate_exprs) + .enumerate() + .map(|(global_index, (item, expr))| { + let target = + item.simple_selector_index.map(selector_target).unwrap_or(QuotientTarget::Main); + QuotientIdentity { + meta: QuotientIdentityMetadata { + global_index, + source: QuotientIdentitySource::Gate { + gate_index: item.gate_index, + gate_name: item.gate_name, + constraint_index: item.constraint_index, + constraint_name: item.constraint_name, + polynomial_index: item.polynomial_index, + }, + }, + lines: item.lines, + var: item.var, + target, + expr, + } + }) + .collect::>(); + let permutation = perm_items + .into_iter() + .enumerate() + .map(|(identity_index, (lines, var))| { + let expr = Self::quotient_yul_expr(&lines, &var); + QuotientIdentity { + meta: QuotientIdentityMetadata { + global_index: permutation_base + identity_index, + source: QuotientIdentitySource::Permutation { identity_index }, + }, + lines, + var, + target: QuotientTarget::Main, + expr, + } + }) + .collect::>(); + let lookup = lookup_items + .into_iter() + .enumerate() + .map(|(identity_index, (lines, var))| { + let expr = Self::quotient_yul_expr(&lines, &var); + let (lookup_index, _) = meta + .protocol + .lookup_identity_source(identity_index) + .expect("lookup identity index covered by protocol lookup chunks"); + let lookup_name = format!("lookup_{lookup_index}"); + QuotientIdentity { + meta: QuotientIdentityMetadata { + global_index: lookup_base + identity_index, + source: QuotientIdentitySource::Lookup { + identity_index, + lookup_index, + lookup_name, + }, + }, + lines, + var, + target: QuotientTarget::Main, + expr, + } + }) + .collect::>(); + let trash = trash_items + .into_iter() + .enumerate() + .map(|(trash_index, (lines, var))| { + let expr = Self::quotient_yul_expr(&lines, &var); + let trash_name = self.trash_manifest_name(trash_index); + QuotientIdentity { + meta: QuotientIdentityMetadata { + global_index: trash_base + trash_index, + source: QuotientIdentitySource::Trash { + trash_index, + trash_name, + }, + }, + lines, + var, + target: QuotientTarget::Main, + expr, + } + }) + .collect::>(); + + assert_eq!( + gates.len(), + meta.protocol.quotient.gates, + "gate identity count must match protocol plan" + ); + assert_eq!( + permutation.len(), + meta.protocol.quotient.permutation, + "permutation identity count must match protocol plan" + ); + assert_eq!( + lookup.len(), + meta.protocol.quotient.lookup, + "lookup identity count must match protocol plan" + ); + assert_eq!( + trash.len(), + meta.protocol.quotient.trash, + "trash identity count must match protocol plan" + ); + + QuotientIdentityParts { + gates, + permutation, + lookup, + trash, + sorted_simple, + } + } + + /// Return the typed quotient expression. + fn quotient_identity_expr(identity: &QuotientIdentity) -> QuotientExpr { + identity.expr.clone() + } + + /// Parse an evaluator-emitted Yul identity into the quotient AST once at + /// plan-construction time. + fn quotient_yul_expr(lines: &[String], var: &str) -> QuotientExpr { + let mut parser = QuotientProgramBuilder::default(); + for line in lines { + parser.assignment(line); + } + parser.parse_expr(var) + } + + /// Lower a Halo2 expression into the quotient AST using generated data. + fn quotient_expr_from_plonk_expr( + meta: &ConstraintSystemMeta, + data: &Data, + expression: &Expression, + ) -> QuotientExpr { + quotient_expr_from_expression(&DataQuotientExpressionEnv { meta, data }, expression) + } + + /// Emit one direct/native quotient identity block and its y-fold side + /// effects. + fn direct_quotient_block( + lines: &[String], + var: &str, + target: QuotientTarget, + selector_gap: Option, + eval_scratch_slot: usize, + state_slots: QuotientStateSlots, + trace: bool, + ) -> Vec { + let mut block = Vec::with_capacity(lines.len() + 6); + block.push("{".to_string()); + let lines = Self::specialize_limb7_chains(lines); + for line in &lines { + block.push(line.clone()); + } + block.push(format!("mstore({eval_scratch_slot:#x}, {var})")); + block.push("}".to_string()); + Self::push_quotient_trace( + &mut block, + state_slots, + format!("mload({eval_scratch_slot:#x})"), + trace, + ); + Self::push_structured_fold_advance(&mut block, 1, "q_direct_fold_i", state_slots); + match target { + QuotientTarget::Main => { + Self::push_quotient_eval_numer_add( + &mut block, + state_slots, + format!("mload({eval_scratch_slot:#x})"), + ); + } + QuotientTarget::Selector(idx) => { + let offset = idx * 0x20; + let gap = selector_gap.expect("selector gap for compact direct quotient block"); + block.push("{".to_string()); + block.push(format!( + "let q_selector_ptr := add(SELECTOR_ACC_MPTR, {offset:#x})" + )); + block.push("let q_selector_acc := mload(q_selector_ptr)".to_string()); + if gap != 0 { + block.push(format!( + "q_selector_acc := mulmod(q_selector_acc, mload(add({:#x}, {:#x})), r)", + state_slots.selector_power_mptr, + gap * WORD_BYTES + )); + } + block.push(format!( + "mstore(q_selector_ptr, addmod(q_selector_acc, mload({eval_scratch_slot:#x}), r))" + )); + block.push("}".to_string()); + } + } + block + } + + /// Append trace emission for a quotient identity value. + fn push_quotient_trace( + block: &mut Vec, + state_slots: QuotientStateSlots, + value: impl AsRef, + trace: bool, + ) { + if !trace { + return; + } + + let value = value.as_ref(); + block.push(format!( + "trace_u256(mload({:#x}), {value})", + state_slots.trace_id_mptr + )); + block.push(format!( + "mstore({:#x}, add(mload({:#x}), 1))", + state_slots.trace_id_mptr, state_slots.trace_id_mptr + )); + } + + /// Add an identity value into the current numerator accumulator. + fn push_quotient_eval_numer_add( + block: &mut Vec, + state_slots: QuotientStateSlots, + value: impl AsRef, + ) { + let value = value.as_ref(); + block.push(format!( + "mstore({:#x}, addmod(mload({:#x}), {value}, r))", + state_slots.eval_numer_mptr, state_slots.eval_numer_mptr + )); + } + + /// Advance the global y-fold state by `count` identity positions. + /// + /// Compact VM mode advances only the main accumulator here: selector + /// buckets use codegen-time gap updates at selector identity positions. + fn push_structured_fold_advance( + block: &mut Vec, + count: usize, + loop_var: &str, + state_slots: QuotientStateSlots, + ) { + let push_one = |block: &mut Vec| { + block.push(format!( + "mstore({:#x}, mulmod(mload({:#x}), y, r))", + state_slots.eval_numer_mptr, state_slots.eval_numer_mptr + )); + }; + + if count == 1 { + push_one(block); + return; + } + + block.push(format!( + "for {{ let {loop_var} := 0 }} lt({loop_var}, {count}) {{ {loop_var} := add({loop_var}, 1) }} {{" + )); + push_one(block); + block.push("}".to_string()); + } + + /// Compact runs of adjacent `mstore(dst+i, mload(src+i*stride))` lines. + /// + /// Native callbacks often stage contiguous eval tables. This helper emits a + /// small copy loop when the staged source and destination offsets form a + /// regular run, otherwise it leaves the original store shape intact. + fn push_mstore_mload_literal_runs( + block: &mut Vec, + dst: &str, + entries: &[(usize, String)], + loop_prefix: &str, + ) { + let mut idx = 0usize; + while idx < entries.len() { + let (dst_base, expr) = &entries[idx]; + let Some(src_base) = yul_mload_literal_expr(expr) else { + block.push(format!("mstore(add({dst}, {dst_base:#x}), {expr})")); + idx += 1; + continue; + }; + let Some((next_dst, next_expr)) = entries.get(idx + 1) else { + block.push(format!("mstore(add({dst}, {dst_base:#x}), {expr})")); + idx += 1; + continue; + }; + if *next_dst != *dst_base + 0x20 { + block.push(format!("mstore(add({dst}, {dst_base:#x}), {expr})")); + idx += 1; + continue; + } + let Some(next_src) = yul_mload_literal_expr(next_expr) else { + block.push(format!("mstore(add({dst}, {dst_base:#x}), {expr})")); + idx += 1; + continue; + }; + let Some(src_stride) = next_src.checked_sub(src_base) else { + block.push(format!("mstore(add({dst}, {dst_base:#x}), {expr})")); + idx += 1; + continue; + }; + if src_stride == 0 { + block.push(format!("mstore(add({dst}, {dst_base:#x}), {expr})")); + idx += 1; + continue; + } + + let mut count = 2usize; + while let Some((candidate_dst, candidate_expr)) = entries.get(idx + count) { + let Some(candidate_src) = yul_mload_literal_expr(candidate_expr) else { + break; + }; + if *candidate_dst != *dst_base + count * 0x20 + || candidate_src != src_base + count * src_stride + { + break; + } + count += 1; + } + + if count < 3 { + block.push(format!("mstore(add({dst}, {dst_base:#x}), {expr})")); + idx += 1; + continue; + } + + block.push("{".to_string()); + block.push(format!( + "for {{ let {loop_prefix}_i := 0 }} lt({loop_prefix}_i, {count}) {{ {loop_prefix}_i := add({loop_prefix}_i, 1) }} {{" + )); + block.push(format!( + "let {loop_prefix}_dst_off := shl(5, {loop_prefix}_i)" + )); + if src_stride == 0x20 { + block.push(format!( + "let {loop_prefix}_src_off := {loop_prefix}_dst_off" + )); + } else { + block.push(format!( + "let {loop_prefix}_src_off := mul({loop_prefix}_i, {src_stride:#x})" + )); + } + block.push(format!( + "mstore(add(add({dst}, {dst_base:#x}), {loop_prefix}_dst_off), mload(add({src_base:#x}, {loop_prefix}_src_off)))" + )); + block.push("}".to_string()); + block.push("}".to_string()); + idx += count; + } + } + + /// Replace recognized seven-limb linear chains with helper calls. + /// + /// This operates on legacy/direct Yul text, not on the compact VM AST. The + /// VM has its own structural limb opcodes; this helper keeps native Yul + /// callbacks compact for the same foreign-field shapes. + pub(crate) fn specialize_limb7_chains(lines: &[String]) -> Vec { + let mut out = Vec::with_capacity(lines.len()); + let mut const_vars = HashMap::new(); + let mut idx = 0usize; + + while idx < lines.len() { + if let Some((consumed, replacement, updated_consts)) = + Self::try_limb7_chain(&lines[idx..], &const_vars, &LIMB7_YUL_COEFFS, "q_limb7") + .or_else(|| { + Self::try_limb7_chain( + &lines[idx..], + &const_vars, + &WIDE_LIMB7_YUL_COEFFS, + "q_limb7_wide", + ) + }) + { + const_vars = updated_consts; + out.extend(replacement); + idx += consumed; + continue; + } + + Self::record_yul_const_assignment(&lines[idx], &mut const_vars); + out.push(lines[idx].clone()); + idx += 1; + } + + out + } + + /// Try to recognize one helper-compatible seven-limb add/mul chain. + fn try_limb7_chain( + lines: &[String], + const_vars: &HashMap, + coeffs: &[&str; 6], + helper_name: &str, + ) -> Option<(usize, Vec, HashMap)> { + let mut idx = 0usize; + let mut keep = Vec::new(); + let mut args = Vec::with_capacity(7); + let mut previous_acc: Option = None; + let mut local_consts = const_vars.clone(); + + for (step, coeff) in coeffs.iter().enumerate() { + let mut skipped = 0usize; + let (mul_dst, mul_arg) = loop { + if idx >= lines.len() || skipped > 4 { + return None; + } + + if let Some((dst, arg)) = + Self::parse_limb7_mul_assignment(&lines[idx], coeff, &local_consts) + { + break (dst, arg); + } + + if yul_let_assignment(&lines[idx]).is_some() { + Self::record_yul_const_assignment(&lines[idx], &mut local_consts); + keep.push(lines[idx].clone()); + idx += 1; + skipped += 1; + } else { + return None; + } + }; + + let (add_dst, lhs, rhs) = yul_addmod_assignment(lines.get(idx + 1)?)?; + let addend = if lhs == mul_dst { + rhs + } else if rhs == mul_dst { + lhs + } else { + return None; + }; + + if step == 0 { + args.push(addend); + } else if Some(addend.as_str()) != previous_acc.as_deref() { + return None; + } + args.push(mul_arg); + previous_acc = Some(add_dst); + idx += 2; + } + + let final_acc = previous_acc?; + keep.push(format!( + "let {final_acc} := {helper_name}({})", + args.join(", ") + )); + Some((idx, keep, local_consts)) + } + + /// Parse one `mulmod(coeff, limb, r)` line in a limb7 chain. + fn parse_limb7_mul_assignment( + line: &str, + expected_coeff: &str, + const_vars: &HashMap, + ) -> Option<(String, String)> { + let (dst, lhs, rhs) = yul_mulmod_assignment(line)?; + if Self::yul_coeff_matches(&lhs, expected_coeff, const_vars) { + Some((dst, rhs)) + } else if Self::yul_coeff_matches(&rhs, expected_coeff, const_vars) { + Some((dst, lhs)) + } else { + None + } + } + + /// Return whether a literal or constant variable equals `expected_coeff`. + fn yul_coeff_matches( + value: &str, + expected_coeff: &str, + const_vars: &HashMap, + ) -> bool { + yul_const_value(value, const_vars).as_deref() == Some(expected_coeff) + } + + /// Record `let name := const` bindings for later limb-chain matching. + fn record_yul_const_assignment(line: &str, const_vars: &mut HashMap) { + let Some((dst, rhs)) = yul_let_assignment(line) else { + return; + }; + let Some(value) = yul_const_value(&rhs, const_vars) else { + return; + }; + const_vars.insert(dst, value); + } + + /// Trace, advance, and accumulate one main quotient identity value. + fn push_structured_main_fold( + block: &mut Vec, + value: impl AsRef, + state_slots: QuotientStateSlots, + trace: bool, + ) { + Self::push_quotient_trace(block, state_slots, value.as_ref(), trace); + Self::push_structured_fold_advance(block, 1, "q_main_fold_i", state_slots); + Self::push_quotient_eval_numer_add(block, state_slots, value.as_ref()); + } + + /// Scratch table width used by the native permutation callback. + fn structured_permutation_scratch_words(meta: &ConstraintSystemMeta) -> usize { + if meta.num_permutation_zs == 0 { + return 0; + } + + let num_cols = meta.permutation_columns.len(); + let num_sets = meta.num_permutation_zs; + // Native permutation code materializes the terms for every permutation + // set into a contiguous scratch table rooted at the callback scratch + // pointer. This is separate from VM operand-stack depth even when both + // regions currently use `quotient_stack_mptr` as their base. + // permutation values, permutation sigma values, z_cur, z_next, + // z_last for every non-final set, and one spill slot for the + // running delta base used by the native permutation callback. + (2 * num_cols) + (2 * num_sets) + num_sets.saturating_sub(1) + 1 + } + + /// Maximum parallel input width across LogUp lookup chunks. + fn structured_lookup_max_parallel(&self, meta: &ConstraintSystemMeta) -> usize { + if meta.num_lookups == 0 { + return 0; + } + + self.vk + .cs() + .lookups() + .iter() + .flat_map(|lookup| { + let chunked = lookup.chunk_by_degree(self.vk.cs().degree()); + chunked + .input_expression_chunks() + .iter() + .map(|input_chunk| input_chunk.len()) + .collect::>() + }) + .max() + .unwrap_or(1) + } + + /// Scratch table width used by the native lookup callback. + fn structured_lookup_scratch_words(&self, meta: &ConstraintSystemMeta) -> usize { + // Native lookup stages f+beta values plus prefix and suffix products. + self.structured_lookup_max_parallel(meta) * 3 + } + + /// Largest scratch table needed by any enabled native VM callback. + pub(super) fn native_callback_scratch_words( + &self, + meta: &ConstraintSystemMeta, + plan: &QuotientProgramPlan, + ) -> usize { + let permutation_words = if plan.has_native_permutation { + Self::structured_permutation_scratch_words(meta) + } else { + 0 + }; + let lookup_words = if plan.has_native_lookup { + self.structured_lookup_scratch_words(meta) + } else { + 0 + }; + permutation_words.max(lookup_words) + } + + /// Emit a native structured loop for the full permutation identity block. + /// + /// The formula follows the upstream permutation verifier/evaluator: + /// first-set boundary, last-set booleanity, set-to-set continuity, and + /// active-row product equality for each chunk. + fn structured_permutation_loop_block( + meta: &ConstraintSystemMeta, + data: &Data, + evaluator: &Evaluator<'_>, + scratch_mptr: usize, + state_slots: QuotientStateSlots, + trace: bool, + ) -> Option> { + if meta.num_permutation_zs == 0 { + return None; + } + + let num_cols = meta.permutation_columns.len(); + let num_sets = meta.num_permutation_zs; + let chunk_len = meta.permutation_chunk_len; + let vals_mptr = scratch_mptr; + let sigmas_mptr = vals_mptr + num_cols * 0x20; + let z_cur_mptr = sigmas_mptr + num_cols * 0x20; + let z_next_mptr = z_cur_mptr + num_sets * 0x20; + let z_last_mptr = z_next_mptr + num_sets * 0x20; + let delta_base_mptr = z_last_mptr + num_sets.saturating_sub(1) * 0x20; + let delta_chunk = Fq::DELTA.pow_vartime([chunk_len as u64]); + let delta_chunk = u256_string(fe_to_u256::(&delta_chunk)); + let delta = fr_delta_literal(); + + let mut block = Vec::new(); + block.push("{".to_string()); + block.push(format!("let delta := {delta}")); + block.push(format!("let q_perm_vals := {vals_mptr:#x}")); + block.push(format!("let q_perm_sigmas := {sigmas_mptr:#x}")); + block.push(format!("let q_perm_z_cur := {z_cur_mptr:#x}")); + block.push(format!("let q_perm_z_next := {z_next_mptr:#x}")); + block.push(format!("let q_perm_z_last := {z_last_mptr:#x}")); + block.push(format!("let q_perm_delta_base_ptr := {delta_base_mptr:#x}")); + block.push(format!("let q_perm_num_cols := {num_cols}")); + block.push(format!("let q_perm_num_sets := {num_sets}")); + block.push(format!("let q_perm_chunk_len := {chunk_len}")); + block.push(format!("let q_perm_delta_chunk := {delta_chunk}")); + + let mut value_entries = Vec::with_capacity(num_cols); + let mut sigma_entries = Vec::with_capacity(num_cols); + for (idx, column) in meta.permutation_columns.iter().enumerate() { + let offset = idx * 0x20; + let value = evaluator.eval_at(column, 0); + let sigma = data + .permutation_evals + .get(column) + .expect("permutation sigma eval present") + .to_string(); + value_entries.push((offset, value)); + sigma_entries.push((offset, sigma)); + } + Self::push_mstore_mload_literal_runs( + &mut block, + "q_perm_vals", + &value_entries, + "q_perm_val_load", + ); + Self::push_mstore_mload_literal_runs( + &mut block, + "q_perm_sigmas", + &sigma_entries, + "q_perm_sigma_load", + ); + + let mut z_cur_entries = Vec::with_capacity(data.permutation_z_evals.len()); + let mut z_next_entries = Vec::with_capacity(data.permutation_z_evals.len()); + let mut z_last_entries = Vec::with_capacity(data.permutation_z_evals.len()); + for (idx, (z_cur, z_next, z_last)) in data.permutation_z_evals.iter().enumerate() { + let offset = idx * 0x20; + z_cur_entries.push((offset, z_cur.to_string())); + z_next_entries.push((offset, z_next.to_string())); + if let Some(z_last) = z_last { + z_last_entries.push((offset, z_last.to_string())); + } + } + Self::push_mstore_mload_literal_runs( + &mut block, + "q_perm_z_cur", + &z_cur_entries, + "q_perm_z_cur_load", + ); + Self::push_mstore_mload_literal_runs( + &mut block, + "q_perm_z_next", + &z_next_entries, + "q_perm_z_next_load", + ); + Self::push_mstore_mload_literal_runs( + &mut block, + "q_perm_z_last", + &z_last_entries, + "q_perm_z_last_load", + ); + + let fold_eval = |block: &mut Vec| { + Self::push_quotient_trace(block, state_slots, "q_perm_eval", trace); + Self::push_structured_fold_advance(block, 1, "q_perm_fold_i", state_slots); + Self::push_quotient_eval_numer_add(block, state_slots, "q_perm_eval"); + }; + + block.push("let q_perm_eval := 0".to_string()); + + block.push( + "q_perm_eval := mulmod(mload(L_0_MPTR), addmod(1, sub(r, mload(q_perm_z_cur)), r), r)" + .to_string(), + ); + fold_eval(&mut block); + + let final_z_offset = (num_sets - 1) * 0x20; + block.push(format!( + "let q_perm_zn := mload(add(q_perm_z_cur, {final_z_offset:#x}))" + )); + block.push( + "q_perm_eval := mulmod(mload(L_LAST_MPTR), addmod(mulmod(q_perm_zn, q_perm_zn, r), sub(r, q_perm_zn), r), r)" + .to_string(), + ); + fold_eval(&mut block); + + if num_sets > 1 { + block.push(format!( + "for {{ let q_perm_i := 1 }} lt(q_perm_i, {num_sets}) {{ q_perm_i := add(q_perm_i, 1) }} {{" + )); + block.push("let q_perm_cur := mload(add(q_perm_z_cur, shl(5, q_perm_i)))".to_string()); + block.push( + "let q_perm_prev := mload(add(q_perm_z_last, shl(5, sub(q_perm_i, 1))))" + .to_string(), + ); + block.push( + "q_perm_eval := mulmod(mload(L_0_MPTR), addmod(q_perm_cur, sub(r, q_perm_prev), r), r)" + .to_string(), + ); + fold_eval(&mut block); + block.push("}".to_string()); + } + + block.push( + "mstore(q_perm_delta_base_ptr, mulmod(mload(BETA_MPTR), mload(X_MPTR), r))".to_string(), + ); + block.push(format!( + "for {{ let q_perm_set := 0 }} lt(q_perm_set, {num_sets}) {{ q_perm_set := add(q_perm_set, 1) }} {{" + )); + block.push("let q_perm_start := mul(q_perm_set, q_perm_chunk_len)".to_string()); + block.push("let q_perm_end := add(q_perm_start, q_perm_chunk_len)".to_string()); + block.push( + "if gt(q_perm_end, q_perm_num_cols) { q_perm_end := q_perm_num_cols }".to_string(), + ); + block.push("let q_perm_left := mload(add(q_perm_z_next, shl(5, q_perm_set)))".to_string()); + block.push("let q_perm_right := mload(add(q_perm_z_cur, shl(5, q_perm_set)))".to_string()); + block.push("let q_perm_delta_pow := mload(q_perm_delta_base_ptr)".to_string()); + block.push("for { let q_perm_j := q_perm_start } lt(q_perm_j, q_perm_end) { q_perm_j := add(q_perm_j, 1) } {".to_string()); + block.push("let q_perm_off := shl(5, q_perm_j)".to_string()); + block.push("let q_perm_v := mload(add(q_perm_vals, q_perm_off))".to_string()); + block.push("let q_perm_s := mload(add(q_perm_sigmas, q_perm_off))".to_string()); + block.push( + "q_perm_left := mulmod(q_perm_left, addmod(addmod(q_perm_v, mulmod(mload(BETA_MPTR), q_perm_s, r), r), mload(GAMMA_MPTR), r), r)" + .to_string(), + ); + block.push( + "q_perm_right := mulmod(q_perm_right, addmod(addmod(q_perm_v, q_perm_delta_pow, r), mload(GAMMA_MPTR), r), r)" + .to_string(), + ); + block.push("q_perm_delta_pow := mulmod(q_perm_delta_pow, delta, r)".to_string()); + block.push("}".to_string()); + block.push( + "q_perm_eval := mulmod(addmod(1, sub(r, addmod(mload(L_LAST_MPTR), mload(L_BLIND_MPTR), r)), r), addmod(q_perm_left, sub(r, q_perm_right), r), r)" + .to_string(), + ); + fold_eval(&mut block); + block.push( + "mstore(q_perm_delta_base_ptr, mulmod(mload(q_perm_delta_base_ptr), q_perm_delta_chunk, r))".to_string(), + ); + block.push("}".to_string()); + + block.push("}".to_string()); + Some(block) + } + + #[allow(clippy::too_many_arguments)] + /// Emit a native structured loop for all LogUp lookup identities. + /// + /// This adapts the upstream LogUp helper and accumulator constraints while + /// staging parallel lookup products in scratch to avoid repeated generated + /// straight-line Yul. + fn structured_lookup_loop_block( + &self, + meta: &ConstraintSystemMeta, + data: &Data, + evaluator: &Evaluator<'_>, + scratch_mptr: usize, + state_slots: QuotientStateSlots, + trace: bool, + ) -> Option> { + if meta.num_lookups == 0 { + return None; + } + + let max_parallel = self.structured_lookup_max_parallel(meta); + + let f_plus_beta_mptr = scratch_mptr; + let prefix_mptr = f_plus_beta_mptr + max_parallel * 0x20; + let suffix_mptr = prefix_mptr + max_parallel * 0x20; + + let mut block = Vec::new(); + block.push("{".to_string()); + block.push(format!("let q_lookup_f := {f_plus_beta_mptr:#x}")); + block.push(format!("let q_lookup_prefix := {prefix_mptr:#x}")); + block.push(format!("let q_lookup_suffix := {suffix_mptr:#x}")); + block.push("let q_lookup_l0 := mload(L_0_MPTR)".to_string()); + block.push("let q_lookup_llast := mload(L_LAST_MPTR)".to_string()); + block.push("let q_lookup_lblind := mload(L_BLIND_MPTR)".to_string()); + block.push("let q_lookup_lsum := addmod(q_lookup_l0, q_lookup_llast, r)".to_string()); + block.push( + "let q_lookup_active := addmod(1, sub(r, addmod(q_lookup_llast, q_lookup_lblind, r)), r)" + .to_string(), + ); + block.push("let q_lookup_beta := mload(BETA_MPTR)".to_string()); + block.push("let q_lookup_theta := mload(THETA_MPTR)".to_string()); + + for (lookup_idx, lookup) in self.vk.cs().lookups().iter().enumerate() { + let chunked = lookup.chunk_by_degree(self.vk.cs().degree()); + let (m_eval, h_evals, z_eval, z_next_eval) = &data.lookup_evals[lookup_idx]; + + block.push("{".to_string()); + + // boundary = (l_0 + l_last) * Z_lookup(x) + block.push("{".to_string()); + block.push(format!( + "let q_lookup_eval := mulmod(q_lookup_lsum, {}, r)", + z_eval + )); + Self::push_structured_main_fold(&mut block, "q_lookup_eval", state_slots, trace); + block.push("}".to_string()); + + for (input_chunk, h_eval) in + chunked.input_expression_chunks().iter().zip(h_evals.iter()) + { + let k = input_chunk.len(); + block.push("{".to_string()); + + if k == 0 { + block.push("let q_lookup_eval := 0".to_string()); + Self::push_structured_main_fold( + &mut block, + "q_lookup_eval", + state_slots, + trace, + ); + block.push("}".to_string()); + continue; + } + + if k == 1 { + evaluator.reset_locals(); + let (mut compressed_lines, compressed_var) = evaluator + .compress_expressions_with_challenge_var(&input_chunk[0], "q_lookup_theta"); + block.append(&mut compressed_lines); + block.push(format!( + "let q_lookup_eval := addmod(mulmod({}, addmod({compressed_var}, q_lookup_beta, r), r), sub(r, 1), r)", + h_eval + )); + Self::push_structured_main_fold( + &mut block, + "q_lookup_eval", + state_slots, + trace, + ); + block.push("}".to_string()); + continue; + } + + evaluator.reset_locals(); + if let Some(mut shared_prefix_lines) = evaluator.lookup_shared_prefix_f_plus_beta( + input_chunk, + "q_lookup_theta", + "q_lookup_beta", + "q_lookup_f", + ) { + block.append(&mut shared_prefix_lines); + } else { + for (input_idx, parallel_input) in input_chunk.iter().enumerate() { + let (mut compressed_lines, compressed_var) = evaluator + .compress_expressions_with_challenge_var( + parallel_input, + "q_lookup_theta", + ); + block.append(&mut compressed_lines); + block.push(format!( + "mstore(add(q_lookup_f, {:#x}), addmod({compressed_var}, q_lookup_beta, r))", + input_idx * 0x20 + )); + } + } + + block.push("let q_lookup_product := 1".to_string()); + block.push(format!( + "for {{ let q_lookup_prod_i := 0 }} lt(q_lookup_prod_i, {k}) {{ q_lookup_prod_i := add(q_lookup_prod_i, 1) }} {{" + )); + block.push( + "q_lookup_product := mulmod(q_lookup_product, mload(add(q_lookup_f, shl(5, q_lookup_prod_i))), r)" + .to_string(), + ); + block.push("}".to_string()); + + block.push("mstore(q_lookup_prefix, 1)".to_string()); + if k > 1 { + block.push(format!( + "for {{ let q_lookup_pref_i := 1 }} lt(q_lookup_pref_i, {k}) {{ q_lookup_pref_i := add(q_lookup_pref_i, 1) }} {{" + )); + block.push("let q_lookup_pref_prev := sub(q_lookup_pref_i, 1)".to_string()); + block.push( + "mstore(add(q_lookup_prefix, shl(5, q_lookup_pref_i)), mulmod(mload(add(q_lookup_prefix, shl(5, q_lookup_pref_prev))), mload(add(q_lookup_f, shl(5, q_lookup_pref_prev))), r))" + .to_string(), + ); + block.push("}".to_string()); + } + + block.push(format!( + "mstore(add(q_lookup_suffix, {:#x}), 1)", + (k - 1) * 0x20 + )); + if k > 1 { + block.push(format!( + "for {{ let q_lookup_suf_i := sub({k}, 1) }} gt(q_lookup_suf_i, 0) {{ q_lookup_suf_i := sub(q_lookup_suf_i, 1) }} {{" + )); + block.push("let q_lookup_suf_prev := sub(q_lookup_suf_i, 1)".to_string()); + block.push( + "mstore(add(q_lookup_suffix, shl(5, q_lookup_suf_prev)), mulmod(mload(add(q_lookup_suffix, shl(5, q_lookup_suf_i))), mload(add(q_lookup_f, shl(5, q_lookup_suf_i))), r))" + .to_string(), + ); + block.push("}".to_string()); + } + + block.push("let q_lookup_sum := 0".to_string()); + block.push(format!( + "for {{ let q_lookup_sum_i := 0 }} lt(q_lookup_sum_i, {k}) {{ q_lookup_sum_i := add(q_lookup_sum_i, 1) }} {{" + )); + block.push( + "q_lookup_sum := addmod(q_lookup_sum, mulmod(mload(add(q_lookup_prefix, shl(5, q_lookup_sum_i))), mload(add(q_lookup_suffix, shl(5, q_lookup_sum_i))), r), r)" + .to_string(), + ); + block.push("}".to_string()); + block.push(format!( + "let q_lookup_eval := addmod(mulmod({}, q_lookup_product, r), sub(r, q_lookup_sum), r)", + h_eval + )); + Self::push_structured_main_fold(&mut block, "q_lookup_eval", state_slots, trace); + block.push("}".to_string()); + } + + // accumulator = + // active * ((Z_next - Z - selector * sum(h)) * (table + beta) + m) + block.push("{".to_string()); + let sum_h_expr = if h_evals.is_empty() { + "0".to_string() + } else { + let sum_h = "q_lookup_sum_h"; + block.push(format!("let {sum_h} := {}", h_evals[0])); + for h_eval in &h_evals[1..] { + block.push(format!("{sum_h} := addmod({sum_h}, {}, r)", h_eval)); + } + sum_h.to_string() + }; + + evaluator.reset_locals(); + let (mut selector_lines, selector_var) = + evaluator.evaluate_expression(chunked.selector_expression()); + block.append(&mut selector_lines); + let (mut table_lines, table_var) = evaluator.compress_expressions_with_challenge_var( + chunked.table_expressions(), + "q_lookup_theta", + ); + block.append(&mut table_lines); + block.push(format!( + "let q_lookup_s_sum_h := mulmod({selector_var}, {sum_h_expr}, r)" + )); + block.push(format!( + "let q_lookup_diff := addmod({}, sub(r, addmod({}, q_lookup_s_sum_h, r)), r)", + z_next_eval, z_eval + )); + block.push(format!( + "let q_lookup_t_beta := addmod({table_var}, q_lookup_beta, r)" + )); + block.push(format!( + "let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), {}, r)", + m_eval + )); + block + .push("let q_lookup_eval := mulmod(q_lookup_active, q_lookup_core, r)".to_string()); + Self::push_structured_main_fold(&mut block, "q_lookup_eval", state_slots, trace); + block.push("}".to_string()); + + block.push("}".to_string()); + } + + block.push("}".to_string()); + Some(block) + } + + /// Emit a native structured loop for the trash identity suffix. + /// + /// Trash constraints compress their expressions with `trash_challenge` and + /// subtract `(1 - selector) * trash_eval`, matching the Rust trash + /// verifier. + fn structured_trash_loop_block( + &self, + meta: &ConstraintSystemMeta, + data: &Data, + evaluator: &Evaluator<'_>, + state_slots: QuotientStateSlots, + trace: bool, + ) -> Option> { + if meta.num_trashcans == 0 { + return None; + } + + let mut block = Vec::new(); + block.push("{".to_string()); + block.push("let q_trash_tau := mload(TRASH_CHALLENGE_MPTR)".to_string()); + + for (idx, argument) in self.vk.cs().trashcans().iter().enumerate() { + block.push("{".to_string()); + evaluator.reset_locals(); + let (mut compressed_lines, compressed_var) = evaluator + .compress_expressions_with_challenge_var( + argument.constraint_expressions(), + "q_trash_tau", + ); + block.append(&mut compressed_lines); + let (mut selector_lines, selector_var) = + evaluator.evaluate_expression(argument.selector()); + block.append(&mut selector_lines); + block.push(format!( + "let q_trash_one_minus_selector := addmod(1, sub(r, {selector_var}), r)" + )); + block.push(format!( + "let q_trash_scaled := mulmod(q_trash_one_minus_selector, {}, r)", + data.trashcan_evals[idx] + )); + block.push(format!( + "let q_trash_eval := addmod({compressed_var}, sub(r, q_trash_scaled), r)" + )); + Self::push_structured_main_fold(&mut block, "q_trash_eval", state_slots, trace); + block.push("}".to_string()); + } + + block.push("}".to_string()); + Some(block) + } + + /// Emit all template blocks for the compact quotient VM representation. + /// + /// The main verifier and standalone quotient evaluator both call this + /// helper, so inline prefixes, native callbacks, structured tails, scratch + /// slots, and trace behavior stay coupled to the same execution plan. + pub(super) fn compact_quotient_computation_blocks( + &self, + meta: &ConstraintSystemMeta, + data: &Data, + quotient_plan: &QuotientProgramPlan, + quotient_stack_mptr: usize, + quotient_state_slots: QuotientStateSlots, + trace: bool, + ) -> QuotientComputationBlocks { + let eval_scratch_slot = quotient_stack_mptr; + let evaluator = Evaluator::new(self.vk.cs(), meta, data).with_pow5_helper(true); + QuotientComputationBlocks { + inline_computations: quotient_plan + .inline_identities + .iter() + .map(|identity| { + Self::direct_quotient_block( + &identity.lines, + &identity.var, + identity.target, + quotient_plan.selector_fold.gap_for(identity), + eval_scratch_slot, + quotient_state_slots, + trace, + ) + }) + .collect(), + eval_numer_computations: Vec::new(), + post_vm_computations: (meta.num_trashcans > 0) + .then(|| { + self.structured_trash_loop_block( + meta, + data, + &evaluator, + quotient_state_slots, + trace, + ) + }) + .flatten() + .into_iter() + .collect(), + native_permutation_computation: quotient_plan + .has_native_permutation + .then(|| { + Self::structured_permutation_loop_block( + meta, + data, + &evaluator, + quotient_stack_mptr, + quotient_state_slots, + trace, + ) + }) + .flatten() + .unwrap_or_default(), + native_lookup_computation: quotient_plan + .has_native_lookup + .then(|| { + self.structured_lookup_loop_block( + meta, + data, + &evaluator, + quotient_stack_mptr, + quotient_state_slots, + trace, + ) + }) + .flatten() + .unwrap_or_default(), + native_identity_computations: quotient_plan + .native_identities + .iter() + .map(|identity| { + Self::direct_quotient_block( + &identity.lines, + &identity.var, + identity.target, + quotient_plan.selector_fold.gap_for(identity), + eval_scratch_slot, + quotient_state_slots, + trace, + ) + }) + .collect(), + } + } + + /// Lower a logical quotient item stream into compact VM bytecode. + pub(super) fn build_quotient_program_items( + &self, + items: &[QuotientProgramItem], + selector_fold: &SelectorFoldPlan, + ) -> QuotientProgramBuild { + let mut builder = QuotientProgramBuilder::default(); + // Lower the logical plan into bytecode in one pass. Repeated + // subexpressions are emitted directly; native callbacks remain opaque + // markers because their arithmetic is emitted as separate Yul kernels + // in the template. + for item in items { + match item { + QuotientProgramItem::Identity(identity) => { + let expr = Self::quotient_identity_expr(identity); + builder.identity_expr(&expr, identity.target, selector_fold.gap_for(identity)); + } + QuotientProgramItem::NativePermutation => builder.native_permutation(), + QuotientProgramItem::NativeLookup => builder.native_lookup(), + QuotientProgramItem::NativeIdentity(native_idx) => { + builder.native_identity(*native_idx); + } + } + } + + builder.finish() + } +} diff --git a/proofs/solidity-verifier/src/lowering/quotient_numerator/mod.rs b/proofs/solidity-verifier/src/lowering/quotient_numerator/mod.rs new file mode 100644 index 000000000..18a7cb60b --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/quotient_numerator/mod.rs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Quotient-numerator planning and emission. +//! +//! This namespace contains the compact VM producer plus the Yul emitter that +//! reconstructs the batched Halo2 identity numerator inside generated +//! Solidity. + +pub(crate) mod vm; +pub(crate) mod yul_emit; + +pub(crate) use yul_emit::Evaluator; diff --git a/proofs/solidity-verifier/src/lowering/quotient_numerator/vm/mod.rs b/proofs/solidity-verifier/src/lowering/quotient_numerator/vm/mod.rs new file mode 100644 index 000000000..affd899a2 --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/quotient_numerator/vm/mod.rs @@ -0,0 +1,3793 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Producer-side implementation of the compact quotient-numerator VM. +//! +//! The verifier reconstructs the batched numerator `nu_y(x)` from the +//! polynomial evaluations that were already read after the Fiat-Shamir +//! challenge `x`, and then opens the linearized commitment at the scalar +//! `-nu_y(x)`. The commitment side contributes `(1 - x^n) * sum_i x_split^i * +//! Q_i` for the quotient limbs, so the scalar stored in verifier memory is +//! deliberately the negated numerator, not `h(x) = nu_y(x) / (x^n - 1)`. +//! +//! This module is the Rust producer for that reconstruction. It lowers Halo2 +//! quotient identities into a compact bytecode stream plus a constant table; +//! `templates/partials/quotient_numerator/QuotientNumeratorBlock.yul` is the +//! only runtime consumer. The VM is therefore an ABI between generated VK data +//! and generated Solidity: opcode numbers, operand widths, memory tokens, stack +//! discipline, fold order, and native callback markers must change in lockstep +//! with the Yul template and the tests that compare the tables. +//! +//! Correctness comes from preserving the same identity stream used by +//! `midnight_proofs::plonk::partially_evaluate_identities` and +//! `compute_linearization_commitment`: +//! +//! ```text +//! identities: e_0, e_1, ..., e_(m-1) +//! main accumulator scan: acc <- acc * y + e_i +//! final main scalar: sum_i e_i * y^(m - 1 - i) +//! ``` +//! +//! The Rust linearization code computes the same scalar by reverse-folding +//! powers of `y`; the Yul VM scans forward with Horner's rule because that is +//! cheaper and streaming-friendly. A simple-selector identity still advances +//! the global `y` position, but its value is sent to the selector commitment +//! bucket instead of the fully evaluated numerator. The selector positions are +//! known at code generation time, so each selector bucket is advanced only by +//! the gap since the previous identity for the same selector, then by its final +//! tail. That preserves the same `y^(m-1-i)` powers without computing `y^-1` +//! or updating every selector bucket position on unrelated identities. +//! +//! The VM is intentionally small rather than general-purpose. It has only Fr +//! arithmetic, memory loads from generated verifier addresses, a deduplicated +//! VK-resident constant table, optional common-subexpression temporaries, and a +//! few fused opcodes for shapes that dominate Midfall quotient identities. +//! Whole-family native markers, such as permutation and lookup callbacks, are +//! domain-shaped superinstructions: they preserve identity order while moving +//! regular product-loop arithmetic out of the interpreter. The limb-aware +//! opcodes are justified as structural compression of foreign-field limb +//! expressions; they do not change the source of truth and they still evaluate +//! the resulting PLONK identity over BLS12-381 Fr. +//! +//! Finalized bytecode is decoded again after run compaction. That safety pass +//! rejects unknown opcodes, truncated operands, unknown memory tokens, stack +//! underflow, and identity-boundary stack leaks before the bytes can be pinned +//! into a VK runtime. + +use std::collections::{HashMap, HashSet}; + +use ff::{Field, PrimeField}; +use midnight_curves::Fq; +use midnight_proofs::plonk::{Expression, Selector}; +use ruint::aliases::U256; + +#[cfg(test)] +use crate::api::{ + QuotientIdentityManifest, QuotientIdentityManifestEntry, QuotientIdentityManifestTarget, +}; +use crate::{ + api::QuotientIdentitySource, + lowering::{ + config, + encoding::{fe_to_u256, ConstraintSystemMeta, Data, Location, Ptr, Value, Word}, + layout::{self, WORD_BYTES}, + }, +}; + +/// Destination of one evaluated quotient identity. +/// +/// Each identity occupies exactly one position in the global `y` batch. The +/// target only decides where the evaluated scalar is accumulated after that +/// position has been consumed. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum QuotientTarget { + /// Fully evaluated identity. + /// + /// The value contributes to the reconstructed numerator scalar. After the + /// whole stream has been consumed, the Yul runtime stores the negation in + /// `QUOTIENT_EVAL_MPTR`. + Main, + /// Simple-selector identity. + /// + /// The `usize` is an index into the sorted simple-selector column list, + /// not the fixed-column index itself. The value is accumulated into the + /// matching selector commitment bucket while still advancing the global + /// `y` batch. + Selector(usize), +} + +/// Source metadata for one internal quotient identity. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct QuotientIdentityMetadata { + /// Position in the global `y`-batch. + pub(crate) global_index: usize, + /// Source family and source-local metadata. + pub(crate) source: QuotientIdentitySource, +} + +/// Complete VM artifact emitted into the generated verifying-key payload. +/// +/// The generator memory planner uses this to reserve constant, bytecode, stack, +/// and scratch regions; the Yul template uses the same values to interpret +/// the program. The build is intentionally self-contained so the external +/// quotient evaluator can execute with only the copied verifier frame plus VK +/// payload data. +#[derive(Clone, Debug)] +pub(crate) struct QuotientProgramBuild { + /// Encoded byte-oriented bytecode. + pub(crate) bytes: Vec, + /// Deduplicated Fr constants addressed by `PUSH_CONST` and fused opcodes. + pub(crate) consts: Vec, + /// Maximum operand-stack depth of the pure interpreted bytecode. + /// + /// Native callbacks may reuse the same stack base as structured scratch, + /// so the generator folds their scratch requirement into the final + /// allocation separately. + pub(crate) max_stack: usize, + /// Opcode bytes actually present in the finalized physical program. + /// + /// The template uses this to render only the interpreter cases reachable + /// by this VK-specialized quotient program. + pub(crate) used_ops: Vec, + /// Memory-token operands actually present in the finalized physical + /// program. + pub(crate) used_mem_tokens: Vec, +} + +/// One Halo2 quotient identity in both native-Yul and VM-ready forms. +/// +/// The normal evaluator still emits assignment lines because direct inline +/// paths and native callbacks reuse them. `expr` is built once during quotient +/// planning and is the single source for compact VM bytecode emission. +#[derive(Clone, Debug)] +pub(crate) struct QuotientIdentity { + /// Host-side diagnostic metadata for this identity. + pub(crate) meta: QuotientIdentityMetadata, + /// Yul assignment lines emitted by the existing evaluator. + pub(crate) lines: Vec, + /// Name of the Yul variable holding the final identity evaluation. + pub(crate) var: String, + /// Accumulator target for this identity. + pub(crate) target: QuotientTarget, + /// Typed expression form used by the VM. + pub(crate) expr: QuotientExpr, +} + +#[cfg(all(test, feature = "evm"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct RepackedProofScalarLayout { + pub(crate) eval_offset: usize, + pub(crate) num_evals: usize, + pub(crate) q_eval_offset: usize, + pub(crate) num_point_sets: usize, +} + +#[derive(Clone, Debug)] +pub(crate) struct RepackedProofLayoutPlan { + /// Counts of compressed G1 commitments read before scalar evaluations. + pub(crate) g1_groups: Vec, + /// Number of ordinary evaluation scalars. + pub(crate) num_evals: usize, + /// Number of quotient-opening point-set scalars. + pub(crate) num_point_sets: usize, +} + +impl RepackedProofLayoutPlan { + /// Build a proof repacking plan from the typed Solidity calldata layout. + pub(crate) fn from_proof_layout( + layout: &crate::lowering::abi::proof::ProofCalldataLayout, + ) -> Self { + Self { + g1_groups: layout.commitment_read_groups(), + num_evals: layout.evals.item_count, + num_point_sets: layout.q_evals.item_count, + } + } + + /// Number of compressed G1 points before the scalar eval block. + pub(crate) fn prefix_g1_count(&self) -> usize { + self.g1_groups.iter().sum() + } + + /// Native compressed proof byte length expected by the repacker. + pub(crate) fn compressed_len(&self) -> usize { + self.prefix_g1_count() * crate::lowering::layout::G1_COMPRESSED_BYTES + + self.num_evals * crate::lowering::layout::WORD_BYTES + + crate::lowering::layout::G1_COMPRESSED_BYTES + + self.num_point_sets * crate::lowering::layout::WORD_BYTES + + crate::lowering::layout::G1_COMPRESSED_BYTES + } + + /// Solidity-facing EIP-2537-padded proof byte length after repacking. + pub(crate) fn repacked_len(&self) -> usize { + self.prefix_g1_count() * crate::lowering::layout::G1_BYTES + + self.num_evals * crate::lowering::layout::WORD_BYTES + + crate::lowering::layout::G1_BYTES + + self.num_point_sets * crate::lowering::layout::WORD_BYTES + + crate::lowering::layout::G1_BYTES + } + + #[cfg(all(test, feature = "evm"))] + /// Return scalar offsets within the repacked proof for tests. + pub(crate) fn scalar_layout(&self) -> RepackedProofScalarLayout { + let eval_offset = self.prefix_g1_count() * crate::lowering::layout::G1_BYTES; + let q_eval_offset = eval_offset + + self.num_evals * crate::lowering::layout::WORD_BYTES + + crate::lowering::layout::G1_BYTES; + RepackedProofScalarLayout { + eval_offset, + num_evals: self.num_evals, + q_eval_offset, + num_point_sets: self.num_point_sets, + } + } +} + +#[derive(Clone, Debug)] +pub(crate) struct QuotientIdentityParts { + /// Gate identities, in the order returned by the upstream constraint + /// system. + pub(crate) gates: Vec, + /// Permutation identities, after gates. + pub(crate) permutation: Vec, + /// Lookup identities, after permutation. + pub(crate) lookup: Vec, + /// Trashcan identities, after lookup. + pub(crate) trash: Vec, + /// Sorted fixed-column indices for simple selectors. + /// + /// VM selector targets use positions in this vector so generated memory + /// buckets are stable even if fixed-column numbers are sparse. + pub(crate) sorted_simple: Vec, +} + +impl QuotientIdentityParts { + /// Return every identity in global y-batch order. + pub(crate) fn all_identities(&self) -> Vec { + self.gates + .iter() + .chain(self.permutation.iter()) + .chain(self.lookup.iter()) + .chain(self.trash.iter()) + .cloned() + .collect() + } + + /// Return the host-side manifest for these identities. + #[cfg(test)] + pub(crate) fn manifest(&self) -> QuotientIdentityManifest { + let entries = self + .gates + .iter() + .chain(self.permutation.iter()) + .chain(self.lookup.iter()) + .chain(self.trash.iter()) + .map(|identity| QuotientIdentityManifestEntry { + global_index: identity.meta.global_index, + source: identity.meta.source.clone(), + target: quotient_manifest_target(identity.target, &self.sorted_simple), + }) + .collect(); + + QuotientIdentityManifest { + entries, + gate_identities: self.gates.len(), + permutation_identities: self.permutation.len(), + lookup_identities: self.lookup.len(), + trash_identities: self.trash.len(), + simple_selector_cols: self.sorted_simple.clone(), + } + } +} + +#[cfg(test)] +fn quotient_manifest_target( + target: QuotientTarget, + sorted_simple: &[usize], +) -> QuotientIdentityManifestTarget { + match target { + QuotientTarget::Main => QuotientIdentityManifestTarget::Main, + QuotientTarget::Selector(selector_index) => QuotientIdentityManifestTarget::Selector { + selector_index, + fixed_column: sorted_simple[selector_index], + }, + } +} + +/// Logical stream item before final bytecode lowering. +/// +/// Native items are not arithmetic opcodes in the Rust builder. They are +/// identity-position markers; the Yul template replaces them with generated +/// callback blocks that perform their own evaluation and fold at exactly the +/// same point in the global `y` batch. +#[derive(Clone, Debug)] +pub(crate) enum QuotientProgramItem { + /// Identity interpreted by the compact VM. + Identity(QuotientIdentity), + /// Generated callback for the whole permutation identity block. + NativePermutation, + /// Generated callback for the whole lookup identity block. + NativeLookup, + /// Generated callback for a selected heavy gate identity. + NativeIdentity(usize), +} + +/// Hybrid execution plan for quotient numerator reconstruction. +/// +/// A small prefix may stay inline, most identities become VM bytecode, and +/// selected expensive shapes can become native callbacks. The plan is a +/// representation choice only: it must preserve the original identity order and +/// the target of every identity. +#[derive(Clone, Debug)] +pub(crate) struct QuotientProgramPlan { + /// Gate prefix emitted directly before the VM loop. + pub(crate) inline_identities: Vec, + /// VM/native stream after the inline prefix. + pub(crate) items: Vec, + /// Bodies for `NativeIdentity` markers, addressed by marker index. + pub(crate) native_identities: Vec, + /// Identity range expanded by a `NativePermutation` marker, if present. + pub(crate) native_permutation_identities: Vec, + /// Identity range expanded by a `NativeLookup` marker, if present. + pub(crate) native_lookup_identities: Vec, + /// Structured post-VM identity range, currently the optional trash suffix. + pub(crate) structured_tail_identities: Vec, + /// Shared selector-column ordering. + pub(crate) sorted_simple: Vec, + /// Whether the stream contains a native permutation callback marker. + pub(crate) has_native_permutation: bool, + /// Whether the stream contains a native lookup callback marker. + pub(crate) has_native_lookup: bool, + /// Gap exponents for simple-selector buckets in the global y-batch. + pub(crate) selector_fold: SelectorFoldPlan, +} + +/// Execution surface used to fold an identity. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum QuotientExecutionKind { + /// Direct Yul emitted before the VM loop. + Inline, + /// Compact VM bytecode expression followed by a fold opcode. + Interpreted, + /// Native callback replacing the full permutation identity range. + NativePermutation, + /// Native callback replacing the full lookup identity range. + NativeLookup, + /// Native callback replacing one selected heavy gate identity. + NativeIdentity { + /// Index into `QuotientProgramPlan::native_identities`. + native_index: usize, + }, + /// Structured Yul emitted after the VM loop. + StructuredTail, +} + +/// One identity after representation choices have been applied. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct QuotientExecutionManifestEntry { + /// Position in the original global y-batch. + pub(crate) global_index: usize, + /// Original source metadata. + pub(crate) source: QuotientIdentitySource, + /// Original fold target. + pub(crate) target: QuotientTarget, + /// How this identity is executed in the generated verifier. + pub(crate) execution: QuotientExecutionKind, +} + +impl QuotientProgramPlan { + /// Reconstruct the generated execution stream back into per-identity folds. + /// + /// Native markers and structured tails are expanded to the exact identity + /// ranges they replace. This is the compiler-correctness bridge between the + /// source identity stream and the mixed VM/native/direct execution plan. + pub(crate) fn execution_manifest(&self) -> Result, String> { + let mut out = Vec::new(); + for identity in &self.inline_identities { + out.push(quotient_execution_entry( + identity, + QuotientExecutionKind::Inline, + )); + } + + for item in &self.items { + match item { + QuotientProgramItem::Identity(identity) => out.push(quotient_execution_entry( + identity, + QuotientExecutionKind::Interpreted, + )), + QuotientProgramItem::NativePermutation => { + if self.native_permutation_identities.is_empty() { + return Err( + "native permutation marker has empty identity range".to_string() + ); + } + out.extend(self.native_permutation_identities.iter().map(|identity| { + quotient_execution_entry(identity, QuotientExecutionKind::NativePermutation) + })); + } + QuotientProgramItem::NativeLookup => { + if self.native_lookup_identities.is_empty() { + return Err("native lookup marker has empty identity range".to_string()); + } + out.extend(self.native_lookup_identities.iter().map(|identity| { + quotient_execution_entry(identity, QuotientExecutionKind::NativeLookup) + })); + } + QuotientProgramItem::NativeIdentity(native_index) => { + let identity = self.native_identities.get(*native_index).ok_or_else(|| { + format!("native identity marker {native_index} has no callback body") + })?; + out.push(quotient_execution_entry( + identity, + QuotientExecutionKind::NativeIdentity { + native_index: *native_index, + }, + )); + } + } + } + + out.extend(self.structured_tail_identities.iter().map(|identity| { + quotient_execution_entry(identity, QuotientExecutionKind::StructuredTail) + })); + Ok(out) + } + + /// Check that representation choices preserve the original identity stream. + pub(crate) fn validate_execution_manifest( + &self, + expected: &[QuotientIdentity], + ) -> Result<(), String> { + let manifest = self.execution_manifest()?; + if manifest.len() != expected.len() { + return Err(format!( + "quotient execution manifest length mismatch: got {}, expected {}", + manifest.len(), + expected.len() + )); + } + + for (pos, (actual, expected)) in manifest.iter().zip(expected).enumerate() { + if actual.global_index != expected.meta.global_index + || actual.source != expected.meta.source + || actual.target != expected.target + { + return Err(format!( + "quotient execution manifest mismatch at position {pos}: got index {}, source {:?}, target {:?}; expected index {}, source {:?}, target {:?}", + actual.global_index, + actual.source, + actual.target, + expected.meta.global_index, + expected.meta.source, + expected.target + )); + } + } + + Ok(()) + } +} + +/// Convert one planned identity plus chosen execution mode into the public +/// execution manifest shape. +fn quotient_execution_entry( + identity: &QuotientIdentity, + execution: QuotientExecutionKind, +) -> QuotientExecutionManifestEntry { + QuotientExecutionManifestEntry { + global_index: identity.meta.global_index, + source: identity.meta.source.clone(), + target: identity.target, + execution, + } +} + +/// Codegen-time selector-bucket schedule for gap-based forward folding. +/// +/// Fully evaluated identities still use the ordinary Horner scan. Selector +/// identities are sparse in that global stream, so each selector bucket only +/// needs to be advanced across the gap since the previous identity for the same +/// selector and then across the final tail after the stream ends. +#[derive(Clone, Debug, Default)] +pub(crate) struct SelectorFoldPlan { + /// `Some(gap)` for selector identities by global identity index. + pub(crate) gaps_by_identity: Vec>, + /// Final y-power tail for each selector bucket. + pub(crate) tail_exponents: Vec, + /// Largest exponent referenced by either gaps or tails. + pub(crate) max_power: usize, +} + +impl SelectorFoldPlan { + /// Gap from this selector identity to the previous identity in the same + /// selector bucket. + pub(crate) fn gap_for(&self, identity: &QuotientIdentity) -> Option { + self.gaps_by_identity.get(identity.meta.global_index).copied().flatten() + } +} + +/// Magic/version word returned by a split quotient evaluator. +pub(crate) const QUOTIENT_EXTERNAL_MAGIC: u64 = 0x5155_4556_414c_0001; +// Coefficients used by generated limb helper snippets in direct/native Yul +// paths. The VM limb opcodes do not hard-code these values; they load the +// corresponding Fr constants from the VK payload so the bytecode remains a +// representation of the actual lowered expression. +pub(crate) const LIMB7_YUL_COEFFS: [&str; layout::quotient_limb::LIN_COEFFS] = [ + "0x100000000000000", + "0x10000000000000000000000000000", + "0x400000000", + "0x40000000000000000000000", + "0x1000", + "0x100000000000000000", +]; +/// Coefficients for seven-term windows in the wide pairwise-product limb basis. +pub(crate) const WIDE_LIMB7_YUL_COEFFS: [&str; layout::quotient_limb::LIN_COEFFS] = [ + "0x100000000000000", + "0x10000000000000000000000000000", + "0x1000000000000000000000000000000000000000000", + "0x100000000000000000000000000000000000000000000000000000000", + "0x6bc66e553973f396854f5626172ba135587d41e37a68209402355093fdcaaf6c", + "0x63f31e3f446953960c9d6964474300df43ab29179970f642a28e39d6c883c74b", +]; + +/// Minimum adjacent fused-op run worth replacing with a run opcode. +pub(crate) const QUOTIENT_VM_RUN_COMPACTION_MIN_LEN: usize = 4; +/// Encoded byte width of a `u16` operand. +pub(crate) const QUOTIENT_VM_BYTE_U16_BYTES: usize = 2; +/// Encoded byte width of the selector-fold `(selector_index, gap)` operand. +pub(crate) const QUOTIENT_VM_BYTE_U24_BYTES: usize = 3; +/// Encoded byte width of absolute memory pointers and token offsets. +pub(crate) const QUOTIENT_VM_BYTE_U32_BYTES: usize = 4; +/// Number of limbs in a recognized foreign-field coordinate. +pub(crate) const QUOTIENT_VM_LIMBS: usize = layout::quotient_limb::LIMBS; +/// Number of pairwise products in a 7x7 limb multiplication grid. +pub(crate) const QUOTIENT_VM_PAIRWISE_TERMS: usize = layout::quotient_limb::PAIRWISE_TERMS; +/// Number of diagonal coefficients in that pairwise grid. +pub(crate) const QUOTIENT_VM_PAIRWISE_COEFFS: usize = layout::quotient_limb::PAIRWISE_COEFFS; + +// Opcode assignments are part of the verifier/VK ABI. +// +// Stack convention: +// * push opcodes place a value in q_top, spilling the previous top to the +// memory stack when needed; +// * generic ADD/MUL consume one spilled operand and q_top, leaving q_top; +// * accumulator opcodes mutate q_top in place and have zero stack effect; +// * FOLD_* consumes q_top and advances the quotient y-batch; +// * native callback markers require an empty stack at identity boundaries. +// +// The numeric assignments are intentionally sparse. Keep 0x1a reserved: +// historical builds used it for an experimental native trash callback, but the +// current VM intentionally has no operation at that value. +pub(crate) const Q_OP_PUSH_CONST: u8 = 0x01; // push VK constant-table value +pub(crate) const Q_OP_PUSH_MEM_LITERAL: u8 = 0x02; // push mload(absolute_ptr) +pub(crate) const Q_OP_PUSH_MEM_TOKEN: u8 = 0x03; // push mload(symbolic_token) +pub(crate) const Q_OP_PUSH_MEM_TOKEN_OFFSET: u8 = 0x04; // push mload(token + offset) +pub(crate) const Q_OP_PUSH_MEM_U16: u8 = 0x05; // push mload(short absolute ptr) +pub(crate) const Q_OP_ADD: u8 = 0x06; // binary Fr addition +pub(crate) const Q_OP_MUL: u8 = 0x07; // binary Fr multiplication +pub(crate) const Q_OP_NEG: u8 = 0x08; // unary Fr negation +pub(crate) const Q_OP_PUSH_CONST_U8: u8 = 0x09; // push short constant-table slot +pub(crate) const Q_OP_FOLD_MAIN: u8 = 0x0a; // consume top into main y-fold +pub(crate) const Q_OP_FOLD_SELECTOR: u8 = 0x0b; // consume top into selector bucket +pub(crate) const Q_OP_ADD_CONST_U8: u8 = 0x0c; // top += short constant +pub(crate) const Q_OP_MUL_CONST_U8: u8 = 0x0d; // top *= short constant +pub(crate) const Q_OP_ADD_CONST: u8 = 0x0e; // top += constant-table value +pub(crate) const Q_OP_MUL_CONST: u8 = 0x0f; // top *= constant-table value +pub(crate) const Q_OP_ADD_MEM_U16: u8 = 0x10; // top += mload(ptr) +pub(crate) const Q_OP_MUL_MEM_U16: u8 = 0x11; // top *= mload(ptr) +pub(crate) const Q_OP_ADD_MUL_MEM_MEM_CONST_U8: u8 = 0x12; // top += lhs * rhs * const +pub(crate) const Q_OP_ADD_MUL_CONST_U8_MEM_U16: u8 = 0x13; // top += const * mload(ptr) +pub(crate) const Q_OP_ADD_MUL_MEM_MEM: u8 = 0x14; // top += lhs * rhs +pub(crate) const Q_OP_RUN_ADD_MUL_MEM_MEM_CONST_U8: u8 = 0x15; // compact run of op 0x12 +pub(crate) const Q_OP_RUN_ADD_MUL_CONST_U8_MEM_U16: u8 = 0x16; // compact run of op 0x13 +pub(crate) const Q_OP_NATIVE_PERMUTATION: u8 = 0x19; // delegate permutation family +pub(crate) const Q_OP_NATIVE_IDENTITY: u8 = 0x1b; // delegate one selected gate identity +pub(crate) const Q_OP_LIN7: u8 = 0x1c; // evaluate seven-limb linear form +pub(crate) const Q_OP_BILIN7_ROW: u8 = 0x1d; // evaluate one limb times a seven-limb row +pub(crate) const Q_OP_BILIN7_PAIRWISE: u8 = 0x1e; // evaluate a 7x7 pairwise product +pub(crate) const Q_OP_NATIVE_LOOKUP: u8 = 0x1f; // delegate lookup family +pub(crate) const Q_OP_POW5: u8 = 0x20; // raise top to the fifth power +pub(crate) const Q_OP_MODARITH7: u8 = 0x21; // dynamic mixed seven-limb affine form +pub(crate) const Q_OP_AFFINE_SUM: u8 = 0x22; // dynamic affine sum over mem/products + +/// MODARITH7 operand flag: first operand is a conditional memory factor. +const Q_MODARITH7_FLAG_COND: u8 = 0x01; +/// MODARITH7 operand flag: a nonzero constant term is encoded first. +const Q_MODARITH7_FLAG_CONST: u8 = 0x02; + +// Memory tokens compress generated Yul symbols whose concrete addresses depend +// on the memory planner. Literal pointers are used for most proof/VK evals; +// tokens cover shared challenge/common-polynomial locations that are easier and +// safer to address symbolically in generated code. +pub(crate) const Q_MEM_L0: u8 = 0x01; // L_0(x) +pub(crate) const Q_MEM_L_LAST: u8 = 0x02; // L_last(x) +pub(crate) const Q_MEM_L_BLIND: u8 = 0x03; // L_blind(x) +pub(crate) const Q_MEM_BETA: u8 = 0x04; // permutation/lookup beta +pub(crate) const Q_MEM_GAMMA: u8 = 0x05; // permutation/lookup gamma +pub(crate) const Q_MEM_X: u8 = 0x06; // evaluation challenge x +pub(crate) const Q_MEM_THETA: u8 = 0x07; // lookup compression theta +pub(crate) const Q_MEM_TRASH_CHALLENGE: u8 = 0x08; // trash argument challenge +pub(crate) const Q_MEM_INSTANCE_EVAL: u8 = 0x09; // locally interpolated public input + +/// Operand decoder classes shared by tests and docs. +/// +/// The Yul template is the runtime decoder; this enum is the compile-time/spec +/// view of the same ABI. Encodings with zero fixed byte length are dynamic +/// byte-only forms. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum QuotientOpcodeEncoding { + None, + U8, + U16, + U24, + U32, + TokenOffset, + AddMulMemMemConstU8, + AddMulConstU8MemU16, + AddMulMemMem, + RunAddMulMemMemConstU8, + RunAddMulConstU8MemU16, + LimbLin, + LimbBilinRow, + LimbBilinPairwise, + LimbModarith7, + AffineSum, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct QuotientOpcodeSpec { + /// Stable snake-case name rendered into template constants. + pub(crate) name: &'static str, + /// Stable opcode byte. + pub(crate) opcode: u8, + /// Byte length in byte-oriented encoding, or zero for dynamic run forms. + pub(crate) byte_len: usize, + /// Operand decoding class. + pub(crate) encoding: QuotientOpcodeEncoding, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct QuotientMemTokenSpec { + /// Generated Yul memory symbol. + pub(crate) name: &'static str, + /// Stable compact token used by VM bytecode. + pub(crate) token: u8, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct QuotientVmSpec { + /// Complete opcode table, used by template constants and ABI tests. + pub(crate) opcodes: &'static [QuotientOpcodeSpec], + /// Complete memory-token table, used by template constants and ABI tests. + pub(crate) mem_tokens: &'static [QuotientMemTokenSpec], + /// Minimum adjacent fused-op run length worth compacting. + pub(crate) run_compaction_min_len: usize, + /// Number of foreign-field limbs recognized by limb opcodes. + pub(crate) limb_count: usize, + /// Number of products in a 7-by-7 pairwise convolution. + pub(crate) limb_pairwise_terms: usize, + /// Number of distinct `i + j` coefficients in that convolution. + pub(crate) limb_pairwise_coeffs: usize, +} + +/// Canonical opcode table shared by Rust tests and rendered template constants. +pub(crate) const QUOTIENT_OPCODE_TABLE: &[QuotientOpcodeSpec] = &[ + QuotientOpcodeSpec { + name: "push_const", + opcode: Q_OP_PUSH_CONST, + byte_len: 1 + QUOTIENT_VM_BYTE_U16_BYTES, + encoding: QuotientOpcodeEncoding::U16, + }, + QuotientOpcodeSpec { + name: "push_mem_literal", + opcode: Q_OP_PUSH_MEM_LITERAL, + byte_len: 1 + QUOTIENT_VM_BYTE_U32_BYTES, + encoding: QuotientOpcodeEncoding::U32, + }, + QuotientOpcodeSpec { + name: "push_mem_token", + opcode: Q_OP_PUSH_MEM_TOKEN, + byte_len: 1 + 1, + encoding: QuotientOpcodeEncoding::U8, + }, + QuotientOpcodeSpec { + name: "push_mem_token_offset", + opcode: Q_OP_PUSH_MEM_TOKEN_OFFSET, + byte_len: 1 + 1 + QUOTIENT_VM_BYTE_U32_BYTES, + encoding: QuotientOpcodeEncoding::TokenOffset, + }, + QuotientOpcodeSpec { + name: "push_mem_u16", + opcode: Q_OP_PUSH_MEM_U16, + byte_len: 1 + QUOTIENT_VM_BYTE_U16_BYTES, + encoding: QuotientOpcodeEncoding::U16, + }, + QuotientOpcodeSpec { + name: "add", + opcode: Q_OP_ADD, + byte_len: 1, + encoding: QuotientOpcodeEncoding::None, + }, + QuotientOpcodeSpec { + name: "mul", + opcode: Q_OP_MUL, + byte_len: 1, + encoding: QuotientOpcodeEncoding::None, + }, + QuotientOpcodeSpec { + name: "neg", + opcode: Q_OP_NEG, + byte_len: 1, + encoding: QuotientOpcodeEncoding::None, + }, + QuotientOpcodeSpec { + name: "push_const_u8", + opcode: Q_OP_PUSH_CONST_U8, + byte_len: 1 + 1, + encoding: QuotientOpcodeEncoding::U8, + }, + QuotientOpcodeSpec { + name: "fold_main", + opcode: Q_OP_FOLD_MAIN, + byte_len: 1, + encoding: QuotientOpcodeEncoding::None, + }, + QuotientOpcodeSpec { + name: "fold_selector", + opcode: Q_OP_FOLD_SELECTOR, + byte_len: 1 + QUOTIENT_VM_BYTE_U24_BYTES, + encoding: QuotientOpcodeEncoding::U24, + }, + QuotientOpcodeSpec { + name: "add_const_u8", + opcode: Q_OP_ADD_CONST_U8, + byte_len: 1 + 1, + encoding: QuotientOpcodeEncoding::U8, + }, + QuotientOpcodeSpec { + name: "mul_const_u8", + opcode: Q_OP_MUL_CONST_U8, + byte_len: 1 + 1, + encoding: QuotientOpcodeEncoding::U8, + }, + QuotientOpcodeSpec { + name: "add_const", + opcode: Q_OP_ADD_CONST, + byte_len: 1 + QUOTIENT_VM_BYTE_U16_BYTES, + encoding: QuotientOpcodeEncoding::U16, + }, + QuotientOpcodeSpec { + name: "mul_const", + opcode: Q_OP_MUL_CONST, + byte_len: 1 + QUOTIENT_VM_BYTE_U16_BYTES, + encoding: QuotientOpcodeEncoding::U16, + }, + QuotientOpcodeSpec { + name: "add_mem_u16", + opcode: Q_OP_ADD_MEM_U16, + byte_len: 1 + QUOTIENT_VM_BYTE_U16_BYTES, + encoding: QuotientOpcodeEncoding::U16, + }, + QuotientOpcodeSpec { + name: "mul_mem_u16", + opcode: Q_OP_MUL_MEM_U16, + byte_len: 1 + QUOTIENT_VM_BYTE_U16_BYTES, + encoding: QuotientOpcodeEncoding::U16, + }, + QuotientOpcodeSpec { + name: "add_mul_mem_mem_const_u8", + opcode: Q_OP_ADD_MUL_MEM_MEM_CONST_U8, + byte_len: 1 + 2 * QUOTIENT_VM_BYTE_U16_BYTES + 1, + encoding: QuotientOpcodeEncoding::AddMulMemMemConstU8, + }, + QuotientOpcodeSpec { + name: "add_mul_const_u8_mem_u16", + opcode: Q_OP_ADD_MUL_CONST_U8_MEM_U16, + byte_len: 1 + QUOTIENT_VM_BYTE_U16_BYTES + 1, + encoding: QuotientOpcodeEncoding::AddMulConstU8MemU16, + }, + QuotientOpcodeSpec { + name: "add_mul_mem_mem", + opcode: Q_OP_ADD_MUL_MEM_MEM, + byte_len: 1 + 2 * QUOTIENT_VM_BYTE_U16_BYTES, + encoding: QuotientOpcodeEncoding::AddMulMemMem, + }, + QuotientOpcodeSpec { + name: "run_add_mul_mem_mem_const_u8", + opcode: Q_OP_RUN_ADD_MUL_MEM_MEM_CONST_U8, + byte_len: 0, + encoding: QuotientOpcodeEncoding::RunAddMulMemMemConstU8, + }, + QuotientOpcodeSpec { + name: "run_add_mul_const_u8_mem_u16", + opcode: Q_OP_RUN_ADD_MUL_CONST_U8_MEM_U16, + byte_len: 0, + encoding: QuotientOpcodeEncoding::RunAddMulConstU8MemU16, + }, + QuotientOpcodeSpec { + name: "native_permutation", + opcode: Q_OP_NATIVE_PERMUTATION, + byte_len: 1, + encoding: QuotientOpcodeEncoding::None, + }, + QuotientOpcodeSpec { + name: "native_lookup", + opcode: Q_OP_NATIVE_LOOKUP, + byte_len: 1, + encoding: QuotientOpcodeEncoding::None, + }, + QuotientOpcodeSpec { + name: "native_identity", + opcode: Q_OP_NATIVE_IDENTITY, + byte_len: 1 + QUOTIENT_VM_BYTE_U16_BYTES, + encoding: QuotientOpcodeEncoding::U16, + }, + QuotientOpcodeSpec { + name: "lin7", + opcode: Q_OP_LIN7, + byte_len: 1 + QUOTIENT_VM_LIMBS * (1 + QUOTIENT_VM_BYTE_U16_BYTES), + encoding: QuotientOpcodeEncoding::LimbLin, + }, + QuotientOpcodeSpec { + name: "bilin7_row", + opcode: Q_OP_BILIN7_ROW, + byte_len: 1 + + QUOTIENT_VM_BYTE_U16_BYTES + + QUOTIENT_VM_LIMBS * (1 + QUOTIENT_VM_BYTE_U16_BYTES), + encoding: QuotientOpcodeEncoding::LimbBilinRow, + }, + QuotientOpcodeSpec { + name: "bilin7_pairwise", + opcode: Q_OP_BILIN7_PAIRWISE, + byte_len: 1 + 2 * QUOTIENT_VM_BYTE_U16_BYTES + QUOTIENT_VM_PAIRWISE_COEFFS, + encoding: QuotientOpcodeEncoding::LimbBilinPairwise, + }, + QuotientOpcodeSpec { + name: "modarith7", + opcode: Q_OP_MODARITH7, + byte_len: 0, + encoding: QuotientOpcodeEncoding::LimbModarith7, + }, + QuotientOpcodeSpec { + name: "pow5", + opcode: Q_OP_POW5, + byte_len: 1, + encoding: QuotientOpcodeEncoding::None, + }, + QuotientOpcodeSpec { + name: "affine_sum", + opcode: Q_OP_AFFINE_SUM, + byte_len: 0, + encoding: QuotientOpcodeEncoding::AffineSum, + }, +]; + +/// Canonical symbolic-memory-token table shared with the Yul VM. +pub(crate) const QUOTIENT_MEM_TOKEN_TABLE: &[QuotientMemTokenSpec] = &[ + QuotientMemTokenSpec { + name: "L_0_MPTR", + token: Q_MEM_L0, + }, + QuotientMemTokenSpec { + name: "L_LAST_MPTR", + token: Q_MEM_L_LAST, + }, + QuotientMemTokenSpec { + name: "L_BLIND_MPTR", + token: Q_MEM_L_BLIND, + }, + QuotientMemTokenSpec { + name: "BETA_MPTR", + token: Q_MEM_BETA, + }, + QuotientMemTokenSpec { + name: "GAMMA_MPTR", + token: Q_MEM_GAMMA, + }, + QuotientMemTokenSpec { + name: "X_MPTR", + token: Q_MEM_X, + }, + QuotientMemTokenSpec { + name: "THETA_MPTR", + token: Q_MEM_THETA, + }, + QuotientMemTokenSpec { + name: "TRASH_CHALLENGE_MPTR", + token: Q_MEM_TRASH_CHALLENGE, + }, + QuotientMemTokenSpec { + name: "INSTANCE_EVAL_MPTR", + token: Q_MEM_INSTANCE_EVAL, + }, +]; + +/// Complete compact VM ABI description used by docs/tests/template rendering. +pub(crate) const QUOTIENT_VM_SPEC: QuotientVmSpec = QuotientVmSpec { + opcodes: QUOTIENT_OPCODE_TABLE, + mem_tokens: QUOTIENT_MEM_TOKEN_TABLE, + run_compaction_min_len: QUOTIENT_VM_RUN_COMPACTION_MIN_LEN, + limb_count: QUOTIENT_VM_LIMBS, + limb_pairwise_terms: QUOTIENT_VM_PAIRWISE_TERMS, + limb_pairwise_coeffs: QUOTIENT_VM_PAIRWISE_COEFFS, +}; + +/// Return the VM opcode spec for a stable opcode byte. +pub(crate) fn quotient_opcode_spec(opcode: u8) -> Option<&'static QuotientOpcodeSpec> { + QUOTIENT_VM_SPEC.opcodes.iter().find(|spec| spec.opcode == opcode) +} + +/// Return the fixed byte length of an opcode in byte-oriented encoding. +/// +/// Dynamic run opcodes intentionally return `None`; callers that need to walk +/// bytecode containing those forms must decode the run count and operand width. +pub(crate) fn quotient_opcode_byte_len(opcode: u8) -> Option { + quotient_opcode_spec(opcode).and_then(|spec| (spec.byte_len != 0).then_some(spec.byte_len)) +} + +/// Convert a little-endian proof scalar into the big-endian 32-byte EVM word. +/// +/// Proof calldata uses scalar-byte order from the host encoding; EVM `mstore` +/// and arithmetic consume canonical big-endian words. +pub(crate) fn scalar_le_to_be_word(bytes: &[u8]) -> [u8; 32] { + assert_eq!(bytes.len(), 32, "scalar proof element must be 32 bytes"); + let mut scalar = [0u8; 32]; + scalar.copy_from_slice(bytes); + scalar.reverse(); + scalar +} + +/// Field-expression AST accepted by the quotient VM builder. +/// +/// This is a deliberately tiny subset of Halo2 expressions and generated Yul: +/// constants, verifier-memory loads, and Fr addition/multiplication/negation. +/// Keeping the tree this small makes bytecode lowering auditable and lets the +/// structural limb recognizers operate without depending on gate names. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum QuotientExpr { + /// Native Fr constant encoded as a `U256`. + Const(U256), + /// Memory-backed verifier value. + Mem(QuotientMem), + /// Fr addition modulo the scalar field. + Add(Box, Box), + /// Fr multiplication modulo the scalar field. + Mul(Box, Box), + /// Fr negation modulo the scalar field. + Neg(Box), +} + +/// Structural foreign-field limb shapes compressed by dedicated opcodes. +/// +/// These are recognized from the expression tree alone. That constraint is +/// important: adding a limb opcode must never make correctness depend on a +/// gate label or on a hand-maintained list of circuit gadgets. If a shape is +/// not recognized exactly, the builder falls back to ordinary Fr VM opcodes. +#[derive(Clone, Debug)] +pub(crate) enum QuotientLimbShape { + // Structural forms from the Midfall foreign-field chips, not gate-name + // dispatch. Rust source shapes: + // circuits/src/field/foreign/util.rs::sum_exprs + // circuits/src/field/foreign/util.rs::pair_wise_prod + // circuits/src/field/foreign/params.rs::base_powers + // circuits/src/field/foreign/params.rs::double_base_powers + // + // These expressions emulate arithmetic modulo a foreign modulus `m` + // inside the circuit, but the verifier still evaluates the resulting + // PLONK identity polynomial over the native BLS12-381 scalar field Fr. + // The coefficients are Fr encodings of base^i mod m or base^(i+j) mod m. + Lin7 { + terms: Vec<(U256, u16)>, + }, + Bilin7Row { + lhs: u16, + terms: Vec<(U256, u16)>, + }, + Bilin7Pairwise { + lhs_base: u16, + rhs_base: u16, + coeffs: Vec, + }, +} + +/// One fused foreign-field/ECC affine identity. +/// +/// This is a bundled version of the same structural limb blocks as +/// `QuotientLimbShape`, plus scalar memory terms and an optional outer +/// condition. It deliberately remains expression-shaped rather than +/// gate-name-shaped: +/// +/// ```text +/// maybe_cond * ( +/// c +/// + sum lin7 blocks +/// + sum row-bilinear blocks +/// + sum pairwise-bilinear blocks +/// + sum scalar_coeff_i * mload(ptr_i) +/// + sum product_coeff_i * mload(lhs_i) * mload(rhs_i) +/// ) +/// ``` +/// +/// Some first-modulus residues reduce to sparse affine product identities with +/// no dense seven-limb block left. Those are still encoded here when they are +/// conditionally gated, which keeps the opcode tied to ModArith-style custom +/// gate checks rather than tiny generic arithmetic snippets. +#[derive(Clone, Debug)] +pub(crate) struct QuotientModarith7Shape { + cond: Option, + constant: U256, + lin: Vec>, + rows: Vec<(u16, Vec<(U256, u16)>)>, + pairwise: Vec<(u16, u16, Vec)>, + mem_terms: Vec<(U256, u16)>, + product_terms: Vec<(U256, u16, u16)>, +} + +/// Compact reference to a verifier memory word. +/// +/// Literal pointers are encoded directly when they fit. Tokens represent +/// generated Yul symbols whose concrete address can change with the memory +/// layout; token offsets support structured regions rooted at those symbols. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) enum QuotientMem { + /// Absolute memory pointer. + Literal(u32), + /// Generated memory symbol token. + Token(u8), + /// Generated memory symbol token plus byte offset. + TokenOffset(u8, u32), +} + +#[derive(Clone, Copy, Debug)] +pub(crate) enum QuotientLeaf { + /// Constant leaf. + Const(U256), + /// Memory leaf. + Mem(QuotientMem), +} + +/// Fused `top += product` forms recognized during expression emission. +/// +/// These are the most common small arithmetic kernels after sum/product +/// flattening. Encoding them as accumulator operations avoids push/load/mul/add +/// sequences and is still easy to reason about because the VM stack effect is +/// exactly zero. +#[derive(Clone, Copy, Debug)] +pub(crate) enum QuotientProductAdd { + /// `top += mload(lhs) * mload(rhs) * const`. + MemMemConstU8 { lhs: u16, rhs: u16, scalar: U256 }, + /// `top += const * mload(ptr)`. + ConstU8Mem { scalar: U256, ptr: u16 }, + /// `top += mload(lhs) * mload(rhs)`. + MemMem { lhs: u16, rhs: u16 }, +} + +/// Lowers quotient identities into VM bytecode and a constant table. +/// +/// The builder owns stack-depth accounting and all byte-level encoding. It +/// emits an expression as an isolated stack computation followed by exactly one +/// fold opcode, so identity boundaries are explicit and native callback markers +/// can be interleaved safely. +pub(crate) struct QuotientProgramBuilder { + // Raw byte-oriented program before run compaction. All stack-depth + // accounting happens against this stream. + pub(crate) bytes: Vec, + pub(crate) consts: Vec, + pub(crate) const_slots: HashMap, + pub(crate) vars: HashMap, + pub(crate) stack_depth: usize, + pub(crate) max_stack: usize, + pub(crate) limb_vm_ops: bool, +} + +impl Default for QuotientProgramBuilder { + /// Start an empty VM program with the crate's default limb-opcode policy. + fn default() -> Self { + Self { + bytes: Vec::new(), + consts: Vec::new(), + const_slots: HashMap::new(), + vars: HashMap::new(), + stack_depth: 0, + max_stack: 0, + limb_vm_ops: config::DEFAULT_QUOTIENT_LIMB_VM_OPS, + } + } +} + +impl QuotientProgramBuilder { + /// Create a builder, optionally enabling limb-specialized opcode emission. + #[cfg(test)] + pub(crate) fn with_limb_vm_ops(enabled: bool) -> Self { + Self { + limb_vm_ops: enabled, + ..Default::default() + } + } + + /// Emit one complete quotient identity and its fold target. + /// + /// The operand stack is reset at the start and must be empty at the end. + /// That invariant is the reason callbacks can share the same stream: the + /// Yul interpreter is allowed to clear `q_top` and reuse `q_sp` whenever a + /// native marker appears between identities. + pub(crate) fn identity_expr( + &mut self, + expr: &QuotientExpr, + target: QuotientTarget, + selector_gap: Option, + ) { + // Each identity is emitted as an isolated stack expression followed by + // one fold opcode. Native callbacks assert the same empty-stack + // boundary, which lets the Yul interpreter reset q_sp before them. + self.vars.clear(); + self.stack_depth = 0; + + self.emit_expr(expr); + self.fold_identity(target, selector_gap); + + assert_eq!(self.stack_depth, 0, "quotient VM stack leak"); + } + + /// Emit the fold opcode for the completed top-of-stack identity value. + fn fold_identity(&mut self, target: QuotientTarget, selector_gap: Option) { + // Mirrors the Rust `compute_linearization_commitment` y-batch: + // every emitted identity is first absorbed into the same running + // power of y, then either accumulated into the fully-evaluated + // numerator or into the simple-selector bucket. + match target { + QuotientTarget::Main => self.op0(Q_OP_FOLD_MAIN), + QuotientTarget::Selector(idx) => { + let gap = selector_gap.expect("selector fold gap for selector identity"); + assert!( + idx <= u8::MAX as usize, + "selector fold selector index exceeds 8 bits" + ); + assert!( + gap <= u16::MAX as usize, + "selector fold gap exceeds 16 bits" + ); + self.bytes.push(Q_OP_FOLD_SELECTOR); + self.u24((idx << 16) | gap); + self.pop_stack(); + } + } + if matches!(target, QuotientTarget::Main) { + self.pop_stack(); + } + } + + /// Emit the native permutation marker. + /// + /// The generated Yul callback evaluates the whole permutation block and + /// performs the same fold side effects that interpreted identities would + /// have performed one by one at this position. + pub(crate) fn native_permutation(&mut self) { + assert_eq!( + self.stack_depth, 0, + "native permutation expects empty VM stack" + ); + // The callback is not an arithmetic stack op: it is a placeholder in + // the y-batched identity stream. The generated Yul block performs its + // own scratch writes and fold calls at this exact program position. + self.bytes.push(Q_OP_NATIVE_PERMUTATION); + } + + /// Emit the native lookup marker. + /// + /// The generated Yul callback evaluates every LogUp lookup identity + /// (boundary, helper chunks, accumulator) and folds each one in the same + /// order as the interpreted identity stream. + pub(crate) fn native_lookup(&mut self) { + assert_eq!(self.stack_depth, 0, "native lookup expects empty VM stack"); + // Like native permutation, this is a domain-shaped superinstruction: + // the opcode marks one family in the y-batched stream while the + // generated callback performs all per-identity arithmetic and folds. + self.bytes.push(Q_OP_NATIVE_LOOKUP); + } + + /// Emit a native heavy-identity marker addressed by `native_idx`. + /// + /// The marker is an execution-plan choice, not a different identity. Its + /// callback body is generated from the same `QuotientIdentity` and must + /// trace and fold exactly once. + pub(crate) fn native_identity(&mut self, native_idx: usize) { + assert_eq!( + self.stack_depth, 0, + "native identity expects empty VM stack" + ); + // Native identity callbacks are a bytecode/gas trade: recognized + // heavy gates stay as generated Yul kernels while the remaining gates + // fall back to the compact VM program stored in the VK payload. + self.bytes.push(Q_OP_NATIVE_IDENTITY); + self.u16(native_idx); + } + + /// Finalize byte-oriented bytecode after compacting adjacent run opcodes. + pub(crate) fn finish(self) -> QuotientProgramBuild { + // `max_stack` is the pure VM operand-stack high-water mark. The memory + // planner adds callback scratch requirements when callbacks share the + // same base pointer. + let bytes = compact_quotient_runs(&self.bytes); + let validated_max_stack = validate_quotient_program(&bytes) + .unwrap_or_else(|err| panic!("invalid finalized quotient VM program: {err}")); + assert_eq!( + validated_max_stack, self.max_stack, + "quotient VM physical program stack depth diverged from builder accounting" + ); + let (used_ops, used_mem_tokens) = quotient_program_usage(&bytes); + QuotientProgramBuild { + bytes, + consts: self.consts, + max_stack: self.max_stack, + used_ops, + used_mem_tokens, + } + } + + /// Parse and remember one generated Yul assignment. + /// + /// This is the bridge for identities that do not yet have typed + /// `Expression` lowering. Only the evaluator's simple arithmetic subset + /// is accepted; unsupported syntax is rejected during code generation. + pub(crate) fn assignment(&mut self, line: &str) { + let assignment = yul_assignment(line) + .unwrap_or_else(|| panic!("unsupported quotient assignment: {}", line.trim())); + let expr = self.parse_expr(&assignment.expr); + self.vars.insert(assignment.dst, expr); + } + + /// Parse a generated Yul expression into `QuotientExpr`. + /// + /// Supported operations are exactly the Fr operations emitted by + /// `Evaluator`: `addmod(_, _, r)`, `mulmod(_, _, r)`, `sub(r, _)`, + /// `mload(_)`, literals, and previously assigned variables. + pub(crate) fn parse_expr(&self, expr: &str) -> QuotientExpr { + let expr = expr.trim(); + if let Some(args) = call_args(expr, "addmod") { + assert_eq!(args.len(), 3, "addmod arity"); + assert_eq!(args[2].trim(), "r", "addmod modulus"); + QuotientExpr::Add( + Box::new(self.parse_expr(&args[0])), + Box::new(self.parse_expr(&args[1])), + ) + } else if let Some(args) = call_args(expr, "mulmod") { + assert_eq!(args.len(), 3, "mulmod arity"); + assert_eq!(args[2].trim(), "r", "mulmod modulus"); + QuotientExpr::Mul( + Box::new(self.parse_expr(&args[0])), + Box::new(self.parse_expr(&args[1])), + ) + } else if let Some(args) = call_args(expr, "sub") { + assert_eq!(args.len(), 2, "sub arity"); + assert_eq!(args[0].trim(), "r", "only sub(r, x) is supported"); + QuotientExpr::Neg(Box::new(self.parse_expr(&args[1]))) + } else if let Some(args) = call_args(expr, "mload") { + assert_eq!(args.len(), 1, "mload arity"); + QuotientExpr::Mem(parse_mem(&args[0])) + } else if is_literal(expr) { + QuotientExpr::Const(parse_u256(expr)) + } else { + self.vars + .get(expr) + .cloned() + .unwrap_or_else(|| panic!("unknown quotient variable: {expr}")) + } + } + + /// Emit a `QuotientExpr` using ordinary VM bytecode plus local peepholes. + /// + /// The method leaves the expression value on top of the VM stack. It first + /// tries structural limb compression, then falls back to accumulator-leaf + /// and fused product-add opcodes before using generic stack add/mul/neg. + pub(crate) fn emit_expr(&mut self, expr: &QuotientExpr) { + if self.try_emit_pow5(expr) { + return; + } + if self.try_emit_modarith7_shape(expr) { + return; + } + if self.try_emit_limb_shape(expr) { + return; + } + if self.try_emit_limb_decomposition(expr) { + return; + } + + match expr { + QuotientExpr::Const(value) => { + self.emit_const(*value); + self.push_stack(); + } + QuotientExpr::Mem(QuotientMem::Literal(ptr)) => { + self.emit_mem_literal(*ptr); + self.push_stack(); + } + QuotientExpr::Mem(QuotientMem::Token(token)) => { + self.bytes.push(Q_OP_PUSH_MEM_TOKEN); + self.bytes.push(*token); + self.push_stack(); + } + QuotientExpr::Mem(QuotientMem::TokenOffset(token, offset)) => { + self.bytes.push(Q_OP_PUSH_MEM_TOKEN_OFFSET); + self.bytes.push(*token); + self.u32(*offset); + self.push_stack(); + } + QuotientExpr::Add(lhs, rhs) => { + if !self.try_emit_add_product(lhs, rhs) && !self.try_emit_add_product(rhs, lhs) { + self.emit_binary_expr( + lhs, + rhs, + Q_OP_ADD, + Q_OP_ADD_CONST_U8, + Q_OP_ADD_CONST, + Q_OP_ADD_MEM_U16, + ); + } + } + QuotientExpr::Mul(lhs, rhs) => { + self.emit_binary_expr( + lhs, + rhs, + Q_OP_MUL, + Q_OP_MUL_CONST_U8, + Q_OP_MUL_CONST, + Q_OP_MUL_MEM_U16, + ); + } + QuotientExpr::Neg(expr) => { + self.emit_expr(expr); + self.op0(Q_OP_NEG); + } + } + } + + /// Try to replace a repeated-factor fifth power with one opcode. + fn try_emit_pow5(&mut self, expr: &QuotientExpr) -> bool { + let Some(base) = quotient_pow5_base(expr) else { + return false; + }; + self.emit_expr(base); + self.bytes.push(Q_OP_POW5); + true + } + + /// Try to extract one limb-specialized subexpression from a larger sum. + fn try_emit_limb_decomposition(&mut self, expr: &QuotientExpr) -> bool { + if !self.limb_vm_ops { + return false; + } + let Some((shape, residue)) = quotient_limb_subshape(expr) else { + return false; + }; + if !self.limb_shape_has_u8_const_slots(&shape) { + return false; + } + self.emit_expr(&residue); + self.emit_limb_shape(shape); + self.op_binary(Q_OP_ADD); + true + } + + /// Try to replace a full expression with one limb-specialized opcode. + /// + /// The const-slot preflight is part of the ABI justification: limb opcodes + /// carry coefficient slots as single bytes, so either every coefficient can + /// be addressed by `u8` after deterministic insertion or the expression + /// must use the generic VM path. + fn try_emit_limb_shape(&mut self, expr: &QuotientExpr) -> bool { + if !self.limb_vm_ops { + return false; + } + + let Some(shape) = quotient_limb_shape(expr) else { + return false; + }; + if !self.limb_shape_has_u8_const_slots(&shape) { + return false; + } + + self.emit_limb_shape(shape); + true + } + + /// Try to replace a whole affine foreign-field/ECC identity with one + /// dynamic byte-only opcode. + fn try_emit_modarith7_shape(&mut self, expr: &QuotientExpr) -> bool { + if !self.limb_vm_ops { + return false; + } + + let Some(shape) = quotient_modarith7_shape(expr) else { + return false; + }; + if !self.modarith7_shape_has_u8_const_slots(&shape) { + return false; + } + + self.emit_modarith7_shape(shape); + true + } + + /// Check whether all coefficients of a fused affine limb identity fit + /// one-byte constant-table slots without mutating the builder. + fn modarith7_shape_has_u8_const_slots(&self, shape: &QuotientModarith7Shape) -> bool { + self.peek_u8_const_slots(&modarith7_coeffs(shape)).is_some() + } + + /// Check whether all coefficients of a recognized limb shape fit `u8` + /// constant slots without mutating the builder. + fn limb_shape_has_u8_const_slots(&self, shape: &QuotientLimbShape) -> bool { + let coeffs = match shape { + QuotientLimbShape::Lin7 { terms } => terms.iter().map(|(coeff, _)| *coeff).collect(), + QuotientLimbShape::Bilin7Row { terms, .. } => { + terms.iter().map(|(coeff, _)| *coeff).collect() + } + QuotientLimbShape::Bilin7Pairwise { coeffs, .. } => coeffs.clone(), + }; + self.peek_u8_const_slots(&coeffs).is_some() + } + + /// Emit the byte-level representation of a pre-validated limb shape. + fn emit_limb_shape(&mut self, shape: QuotientLimbShape) { + match shape { + QuotientLimbShape::Lin7 { terms } => { + // LIN7 is the VM encoding of: + // sum_exprs(base_powers, limbs) + // from foreign-field normalization/multiplication and EC + // gates. It packs seven limb-evaluation loads and their + // generated Fr coefficients into one interpreter opcode. + self.bytes.push(Q_OP_LIN7); + for (coeff, ptr) in terms { + let slot = self.const_slot(coeff); + let slot = u8::try_from(slot).expect("lin7 const slot checked"); + self.bytes.push(slot); + self.u16(ptr as usize); + } + } + QuotientLimbShape::Bilin7Row { lhs, terms } => { + // BILIN7_ROW captures the repeated row shape + // lhs * sum_i coeff[i] * rhs[i]. It appears after lowering + // pair_wise_prod slices in foreign-field multiplication and + // EC slope/tangent/on-curve/lambda-squared identities. + self.bytes.push(Q_OP_BILIN7_ROW); + self.u16(lhs as usize); + for (coeff, rhs) in terms { + let slot = self.const_slot(coeff); + let slot = u8::try_from(slot).expect("bilin7 row const slot checked"); + self.bytes.push(slot); + self.u16(rhs as usize); + } + } + QuotientLimbShape::Bilin7Pairwise { + lhs_base, + rhs_base, + coeffs, + } => { + // BILIN7_PAIRWISE captures the full 7-by-7 convolution: + // sum_{i,j} coeff[i+j] * lhs[i] * rhs[j] + // matching sum_exprs(double_base_powers, + // pair_wise_prod(lhs, rhs)). + self.bytes.push(Q_OP_BILIN7_PAIRWISE); + self.u16(lhs_base as usize); + self.u16(rhs_base as usize); + for coeff in coeffs { + let slot = self.const_slot(coeff); + let slot = u8::try_from(slot).expect("bilin7 pairwise const slot checked"); + self.bytes.push(slot); + } + } + } + self.push_stack(); + } + + /// Emit the dynamic byte-level representation of one fused affine + /// foreign-field/ECC identity. + fn emit_modarith7_shape(&mut self, shape: QuotientModarith7Shape) { + self.bytes.push(Q_OP_MODARITH7); + + let mut flags = 0u8; + if shape.cond.is_some() { + flags |= Q_MODARITH7_FLAG_COND; + } + if shape.constant != U256::ZERO { + flags |= Q_MODARITH7_FLAG_CONST; + } + self.bytes.push(flags); + + if let Some(cond) = shape.cond { + self.u16(cond as usize); + } + if shape.constant != U256::ZERO { + let slot = self.const_slot(shape.constant); + let slot = u8::try_from(slot).expect("modarith7 const slot checked"); + self.bytes.push(slot); + } + + let lin_count = u8::try_from(shape.lin.len()).expect("modarith7 lin count checked"); + let row_count = u8::try_from(shape.rows.len()).expect("modarith7 row count checked"); + let pairwise_count = + u8::try_from(shape.pairwise.len()).expect("modarith7 pairwise count checked"); + let mem_count = u8::try_from(shape.mem_terms.len()).expect("modarith7 mem count checked"); + let product_count = + u8::try_from(shape.product_terms.len()).expect("modarith7 product count checked"); + self.bytes.push(lin_count); + self.bytes.push(row_count); + self.bytes.push(pairwise_count); + self.bytes.push(mem_count); + self.bytes.push(product_count); + + for terms in shape.lin { + for (coeff, ptr) in terms { + let slot = self.const_slot(coeff); + let slot = u8::try_from(slot).expect("modarith7 lin const slot checked"); + self.bytes.push(slot); + self.u16(ptr as usize); + } + } + for (lhs, terms) in shape.rows { + self.u16(lhs as usize); + for (coeff, rhs) in terms { + let slot = self.const_slot(coeff); + let slot = u8::try_from(slot).expect("modarith7 row const slot checked"); + self.bytes.push(slot); + self.u16(rhs as usize); + } + } + for (lhs_base, rhs_base, coeffs) in shape.pairwise { + self.u16(lhs_base as usize); + self.u16(rhs_base as usize); + for coeff in coeffs { + let slot = self.const_slot(coeff); + let slot = u8::try_from(slot).expect("modarith7 pairwise const slot checked"); + self.bytes.push(slot); + } + } + for (coeff, ptr) in shape.mem_terms { + let slot = self.const_slot(coeff); + let slot = u8::try_from(slot).expect("modarith7 mem const slot checked"); + self.bytes.push(slot); + self.u16(ptr as usize); + } + for (coeff, lhs, rhs) in shape.product_terms { + let slot = self.const_slot(coeff); + let slot = u8::try_from(slot).expect("modarith7 product const slot checked"); + self.bytes.push(slot); + self.u16(lhs as usize); + self.u16(rhs as usize); + } + + self.push_stack(); + } + + /// Emit a constant load, choosing the shortest constant-slot operand. + fn emit_const(&mut self, value: U256) { + let slot = self.const_slot(value); + if let Ok(slot) = u8::try_from(slot) { + self.bytes.push(Q_OP_PUSH_CONST_U8); + self.bytes.push(slot); + } else { + self.bytes.push(Q_OP_PUSH_CONST); + self.u16(slot as usize); + } + } + + /// Emit a memory load, choosing the shortest literal-pointer operand. + fn emit_mem_literal(&mut self, ptr: u32) { + if let Ok(ptr) = u16::try_from(ptr) { + self.bytes.push(Q_OP_PUSH_MEM_U16); + self.u16(ptr as usize); + } else { + self.bytes.push(Q_OP_PUSH_MEM_LITERAL); + self.u32(ptr); + } + } + + /// Try to emit `base + product` as a fused accumulator opcode. + fn try_emit_add_product(&mut self, base: &QuotientExpr, product: &QuotientExpr) -> bool { + let mut leaves = Vec::new(); + if !collect_product_leaves(product, &mut leaves) { + return false; + } + let Some(product) = self.product_add_macro(&leaves) else { + return false; + }; + + self.emit_expr(base); + self.emit_product_add(product); + true + } + + /// Recognize product leaves that can be encoded as one fused add-mul op. + /// + /// The fused forms require literal `u16` memory pointers and, where a + /// constant is present, a coefficient that can fit a `u8` const slot. Token + /// memory is kept on the generic path because the fused byte layout stores + /// raw pointers only. + fn product_add_macro(&self, leaves: &[QuotientLeaf]) -> Option { + let mut mems = Vec::new(); + let mut consts = Vec::new(); + for leaf in leaves { + match *leaf { + QuotientLeaf::Mem(QuotientMem::Literal(ptr)) => { + mems.push(u16::try_from(ptr).ok()?); + } + QuotientLeaf::Const(value) => { + if !self.const_fits_u8_slot(value) { + return None; + } + consts.push(value); + } + QuotientLeaf::Mem(QuotientMem::Token(_)) + | QuotientLeaf::Mem(QuotientMem::TokenOffset(_, _)) => return None, + } + } + + match (mems.as_slice(), consts.as_slice()) { + ([lhs, rhs], [scalar]) => Some(QuotientProductAdd::MemMemConstU8 { + lhs: *lhs, + rhs: *rhs, + scalar: *scalar, + }), + ([ptr], [scalar]) => Some(QuotientProductAdd::ConstU8Mem { + scalar: *scalar, + ptr: *ptr, + }), + ([lhs, rhs], []) => Some(QuotientProductAdd::MemMem { + lhs: *lhs, + rhs: *rhs, + }), + _ => None, + } + } + + /// Emit one fused add-mul accumulator operation. + fn emit_product_add(&mut self, product: QuotientProductAdd) { + match product { + QuotientProductAdd::MemMemConstU8 { lhs, rhs, scalar } => { + let slot = self.const_slot(scalar); + let slot = u8::try_from(slot).expect("const slot checked"); + self.bytes.push(Q_OP_ADD_MUL_MEM_MEM_CONST_U8); + self.u16(lhs as usize); + self.u16(rhs as usize); + self.bytes.push(slot); + } + QuotientProductAdd::ConstU8Mem { scalar, ptr } => { + let slot = self.const_slot(scalar); + let slot = u8::try_from(slot).expect("const slot checked"); + self.bytes.push(Q_OP_ADD_MUL_CONST_U8_MEM_U16); + self.u16(ptr as usize); + self.bytes.push(slot); + } + QuotientProductAdd::MemMem { lhs, rhs } => { + self.bytes.push(Q_OP_ADD_MUL_MEM_MEM); + self.u16(lhs as usize); + self.u16(rhs as usize); + } + } + } + + /// Emit a binary expression with accumulator-leaf peepholes. + /// + /// If one side is a constant or short memory load, the VM can update the + /// other side's top-of-stack directly with `ADD_CONST`, `MUL_MEM_U16`, and + /// related opcodes. Otherwise both operands are pushed and a generic stack + /// operation combines them. + fn emit_binary_expr( + &mut self, + lhs: &QuotientExpr, + rhs: &QuotientExpr, + stack_op: u8, + const_u8_op: u8, + const_op: u8, + mem_u16_op: u8, + ) { + if let Some(leaf) = quotient_leaf(rhs) { + self.emit_expr(lhs); + if self.emit_acc_leaf(leaf, const_u8_op, const_op, mem_u16_op) { + return; + } + self.emit_expr(rhs); + self.op_binary(stack_op); + return; + } + if let Some(leaf) = quotient_leaf(lhs) { + self.emit_expr(rhs); + if self.emit_acc_leaf(leaf, const_u8_op, const_op, mem_u16_op) { + return; + } + self.emit_expr(lhs); + self.op_binary(stack_op); + return; + } + + self.emit_expr(lhs); + self.emit_expr(rhs); + self.op_binary(stack_op); + } + + /// Try to apply a constant or short-memory accumulator opcode to `q_top`. + fn emit_acc_leaf( + &mut self, + leaf: QuotientLeaf, + const_u8_op: u8, + const_op: u8, + mem_u16_op: u8, + ) -> bool { + match leaf { + QuotientLeaf::Const(value) => { + let slot = self.const_slot(value); + if let Ok(slot) = u8::try_from(slot) { + self.bytes.push(const_u8_op); + self.bytes.push(slot); + } else { + self.bytes.push(const_op); + self.u16(slot as usize); + } + true + } + QuotientLeaf::Mem(QuotientMem::Literal(ptr)) => { + if let Ok(ptr) = u16::try_from(ptr) { + self.bytes.push(mem_u16_op); + self.u16(ptr as usize); + true + } else { + false + } + } + QuotientLeaf::Mem(QuotientMem::Token(_)) + | QuotientLeaf::Mem(QuotientMem::TokenOffset(_, _)) => false, + } + } + + /// Emit a zero-operand opcode. + fn op0(&mut self, op: u8) { + self.bytes.push(op); + } + + /// Emit a binary stack opcode and update stack-depth accounting. + fn op_binary(&mut self, op: u8) { + self.bytes.push(op); + self.pop_stack(); + } + + /// Record one pushed stack value and update the high-water mark. + fn push_stack(&mut self) { + self.stack_depth += 1; + self.max_stack = self.max_stack.max(self.stack_depth); + } + + /// Record one consumed stack value. + fn pop_stack(&mut self) { + self.stack_depth = self.stack_depth.checked_sub(1).expect("quotient VM stack underflow"); + } + + /// Append a big-endian `u16` operand. + fn u16(&mut self, value: usize) { + assert!(value <= u16::MAX as usize, "quotient VM u16 overflow"); + self.bytes.extend_from_slice(&(value as u16).to_be_bytes()); + } + + /// Append a big-endian 24-bit operand. + fn u24(&mut self, value: usize) { + assert!(value <= 0x00ff_ffff, "quotient VM u24 overflow"); + self.bytes.extend_from_slice(&(value as u32).to_be_bytes()[1..]); + } + + /// Append a big-endian `u32` operand. + fn u32(&mut self, value: u32) { + self.bytes.extend_from_slice(&value.to_be_bytes()); + } + + /// Return the stable constant-table slot for `value`, inserting if needed. + fn const_slot(&mut self, value: U256) -> u16 { + if let Some(slot) = self.const_slots.get(&value) { + *slot + } else { + let slot = self.consts.len(); + assert!(slot <= u16::MAX as usize, "too many quotient constants"); + self.consts.push(value); + self.const_slots.insert(value, slot as u16); + slot as u16 + } + } + + /// Check whether `value` can be addressed by a one-byte constant slot. + fn const_fits_u8_slot(&self, value: U256) -> bool { + self.const_slots.get(&value).is_some_and(|slot| u8::try_from(*slot).is_ok()) + || (!self.const_slots.contains_key(&value) && self.consts.len() <= u8::MAX as usize) + } + + /// Predict one-byte constant slots for a batch of values without insertion. + /// + /// This lets limb opcode recognition fail cleanly before mutating the + /// constant table, so fallback lowering sees the same builder state it + /// would have seen if recognition had never been attempted. + fn peek_u8_const_slots(&self, values: &[U256]) -> Option> { + let mut next_slot = self.consts.len(); + let mut pending = HashMap::new(); + let mut slots = Vec::with_capacity(values.len()); + for value in values { + let slot = if let Some(slot) = self.const_slots.get(value).copied() { + slot as usize + } else if let Some(slot) = pending.get(value).copied() { + slot + } else { + let slot = next_slot; + next_slot += 1; + pending.insert(*value, slot); + slot + }; + slots.push(u8::try_from(slot).ok()?); + } + Some(slots) + } +} + +/// Compact long adjacent fused-op runs in byte-oriented VM encoding. +pub(crate) fn compact_quotient_runs(bytes: &[u8]) -> Vec { + // Run compaction is only a byte-encoding optimization. It preserves the + // logical operation stream by replacing long adjacent fused add-mul ops + // with one counted opcode followed by the same operands. Mixed linear and + // bilinear affine runs use a larger superinstruction because addition is + // commutative and each term only mutates the same accumulator. + let mut out = Vec::with_capacity(bytes.len()); + let mut idx = 0usize; + while idx < bytes.len() { + let op = bytes[idx]; + if is_affine_add_op(op) { + let run_start = idx; + let mut term_ops = Vec::new(); + let mut lin_count = 0usize; + let mut product_count = 0usize; + while idx < bytes.len() && is_affine_add_op(bytes[idx]) { + let term_op = bytes[idx]; + let len = quotient_op_len(bytes, idx); + term_ops.push((term_op, idx + 1, len - 1)); + match term_op { + Q_OP_ADD_MUL_CONST_U8_MEM_U16 => lin_count += 1, + Q_OP_ADD_MUL_MEM_MEM_CONST_U8 => product_count += 1, + _ => {} + } + idx += len; + if lin_count == u16::MAX as usize || product_count == u16::MAX as usize { + break; + } + } + + if lin_count > 0 + && product_count > 0 + && lin_count + product_count >= QUOTIENT_VM_SPEC.run_compaction_min_len + { + out.push(Q_OP_AFFINE_SUM); + out.extend_from_slice(&(lin_count as u16).to_be_bytes()); + out.extend_from_slice(&(product_count as u16).to_be_bytes()); + for (term_op, term_start, term_len) in &term_ops { + if *term_op == Q_OP_ADD_MUL_CONST_U8_MEM_U16 { + out.extend_from_slice(&bytes[*term_start..*term_start + *term_len]); + } + } + for (term_op, term_start, term_len) in &term_ops { + if *term_op == Q_OP_ADD_MUL_MEM_MEM_CONST_U8 { + out.extend_from_slice(&bytes[*term_start..*term_start + *term_len]); + } + } + } else if term_ops.len() >= QUOTIENT_VM_SPEC.run_compaction_min_len + && term_ops.iter().all(|(term_op, _, _)| *term_op == op) + && matches!( + op, + Q_OP_ADD_MUL_MEM_MEM_CONST_U8 | Q_OP_ADD_MUL_CONST_U8_MEM_U16 + ) + { + let run_operands_len = term_ops[0].2; + let run_op = match op { + Q_OP_ADD_MUL_MEM_MEM_CONST_U8 => Q_OP_RUN_ADD_MUL_MEM_MEM_CONST_U8, + Q_OP_ADD_MUL_CONST_U8_MEM_U16 => Q_OP_RUN_ADD_MUL_CONST_U8_MEM_U16, + _ => unreachable!(), + }; + out.push(run_op); + out.extend_from_slice(&(term_ops.len() as u16).to_be_bytes()); + for (_, term_start, _) in &term_ops { + out.extend_from_slice(&bytes[*term_start..*term_start + run_operands_len]); + } + } else { + out.extend_from_slice(&bytes[run_start..idx]); + } + } else { + let len = quotient_op_len(bytes, idx); + out.extend_from_slice(&bytes[idx..idx + len]); + idx += len; + } + } + out +} + +/// Whether an opcode contributes a fused affine-add term to the current top. +fn is_affine_add_op(op: u8) -> bool { + matches!( + op, + Q_OP_ADD_MUL_MEM_MEM_CONST_U8 | Q_OP_ADD_MUL_CONST_U8_MEM_U16 + ) +} + +/// Return opcode and memory-token usage for a finalized quotient program. +/// +/// This is a code-size optimization for the VK-specialized interpreter. The +/// generated evaluator only needs switch arms for opcodes that can actually +/// occur in its embedded program; malformed external frames still hit the +/// default revert branch. +pub(crate) fn quotient_program_usage(bytes: &[u8]) -> (Vec, Vec) { + let mut ops = Vec::new(); + let mut mem_tokens = Vec::new(); + + for (idx, op, _) in quotient_bytecode_ops(bytes) { + push_unique_u8(&mut ops, op); + match op { + Q_OP_PUSH_MEM_TOKEN => { + push_unique_u8(&mut mem_tokens, bytes[idx + 1]); + } + Q_OP_PUSH_MEM_TOKEN_OFFSET => { + push_unique_u8(&mut mem_tokens, bytes[idx + 1]); + } + _ => {} + } + } + + (ops, mem_tokens) +} + +/// Insert a byte once and keep the set sorted for deterministic templates. +fn push_unique_u8(values: &mut Vec, value: u8) { + if !values.contains(&value) { + values.push(value); + values.sort_unstable(); + } +} + +/// Decode a finalized byte-oriented quotient program and verify stack safety. +/// +/// This is an offline safety check for the VK-pinned program, not a runtime +/// verifier feature. The Yul VM stays lean and assumes codegen emitted a valid +/// stream; this pass proves that assumption before the program is embedded. +pub(crate) fn validate_quotient_program(bytes: &[u8]) -> Result { + let mut idx = 0usize; + let mut depth = 0usize; + let mut max_stack = 0usize; + + while idx < bytes.len() { + let (op, len) = decode_byte_quotient_instruction(bytes, idx)?; + apply_quotient_stack_effect(op, idx, &mut depth, &mut max_stack)?; + idx += len; + } + + if depth != 0 { + return Err(format!( + "quotient VM stack leak at end of program: {depth} value(s) remain" + )); + } + + Ok(max_stack) +} + +/// Decode one instruction and validate token operands. +fn decode_byte_quotient_instruction(bytes: &[u8], idx: usize) -> Result<(u8, usize), String> { + require_quotient_bytes(bytes, idx, 1, "opcode")?; + let op = bytes[idx]; + let spec = quotient_opcode_spec(op) + .ok_or_else(|| format!("unknown quotient VM opcode {op:#x} at byte {idx}"))?; + let len = quotient_byte_instruction_len_checked(bytes, idx)?; + + match op { + Q_OP_PUSH_MEM_TOKEN => { + let token = bytes[idx + 1]; + validate_quotient_mem_token(token, idx)?; + } + Q_OP_PUSH_MEM_TOKEN_OFFSET => { + let token = bytes[idx + 1]; + validate_quotient_mem_token(token, idx)?; + } + _ => {} + } + + if len == 0 { + return Err(format!( + "quotient VM opcode {} ({op:#x}) decoded to zero length at byte {idx}", + spec.name + )); + } + Ok((op, len)) +} + +/// Return one instruction length while checking dynamic operand bounds. +fn quotient_byte_instruction_len_checked(bytes: &[u8], idx: usize) -> Result { + let op = bytes[idx]; + let len = match op { + Q_OP_RUN_ADD_MUL_MEM_MEM_CONST_U8 => { + require_quotient_bytes(bytes, idx, 1 + QUOTIENT_VM_BYTE_U16_BYTES, "run count")?; + let count = read_u16(bytes, idx + 1) as usize; + if count == 0 { + return Err(format!("zero-length quotient VM run at byte {idx}")); + } + let payload_len = + checked_quotient_len_mul(count, 2 * QUOTIENT_VM_BYTE_U16_BYTES + 1, op, idx)?; + checked_quotient_len_add(1 + QUOTIENT_VM_BYTE_U16_BYTES, payload_len, op, idx)? + } + Q_OP_RUN_ADD_MUL_CONST_U8_MEM_U16 => { + require_quotient_bytes(bytes, idx, 1 + QUOTIENT_VM_BYTE_U16_BYTES, "run count")?; + let count = read_u16(bytes, idx + 1) as usize; + if count == 0 { + return Err(format!("zero-length quotient VM run at byte {idx}")); + } + let payload_len = + checked_quotient_len_mul(count, QUOTIENT_VM_BYTE_U16_BYTES + 1, op, idx)?; + checked_quotient_len_add(1 + QUOTIENT_VM_BYTE_U16_BYTES, payload_len, op, idx)? + } + Q_OP_AFFINE_SUM => quotient_affine_sum_op_len_checked(bytes, idx)?, + Q_OP_MODARITH7 => quotient_modarith7_op_len_checked(bytes, idx)?, + _ => quotient_opcode_byte_len(op) + .ok_or_else(|| format!("unknown quotient VM opcode {op:#x} at byte {idx}"))?, + }; + require_quotient_bytes(bytes, idx, len, "instruction")?; + Ok(len) +} + +/// Decode the dynamic `AFFINE_SUM` payload header and byte length. +fn quotient_affine_sum_op_len_checked(bytes: &[u8], idx: usize) -> Result { + let op = Q_OP_AFFINE_SUM; + require_quotient_bytes( + bytes, + idx, + 1 + 2 * QUOTIENT_VM_BYTE_U16_BYTES, + "AFFINE_SUM count header", + )?; + let lin_count = read_u16(bytes, idx + 1) as usize; + let product_count = read_u16(bytes, idx + 3) as usize; + if lin_count == 0 || product_count == 0 { + return Err(format!( + "AFFINE_SUM at byte {idx} requires nonzero linear and product counts" + )); + } + let lin_payload = checked_quotient_len_mul(lin_count, QUOTIENT_VM_BYTE_U16_BYTES + 1, op, idx)?; + let product_payload = + checked_quotient_len_mul(product_count, 2 * QUOTIENT_VM_BYTE_U16_BYTES + 1, op, idx)?; + let payload = checked_quotient_len_add(lin_payload, product_payload, op, idx)?; + checked_quotient_len_add(1 + 2 * QUOTIENT_VM_BYTE_U16_BYTES, payload, op, idx) +} + +/// Decode the dynamic `MODARITH7` payload and byte length. +fn quotient_modarith7_op_len_checked(bytes: &[u8], idx: usize) -> Result { + let op = Q_OP_MODARITH7; + let mut cursor = idx; + advance_quotient_cursor(bytes, &mut cursor, 1, op, idx)?; // opcode + require_quotient_bytes(bytes, cursor, 1, "MODARITH7 flags")?; + let flags = bytes[cursor]; + if flags & !(Q_MODARITH7_FLAG_COND | Q_MODARITH7_FLAG_CONST) != 0 { + return Err(format!( + "MODARITH7 has unknown flag bits {flags:#x} at byte {idx}" + )); + } + advance_quotient_cursor(bytes, &mut cursor, 1, op, idx)?; + if flags & Q_MODARITH7_FLAG_COND != 0 { + advance_quotient_cursor(bytes, &mut cursor, QUOTIENT_VM_BYTE_U16_BYTES, op, idx)?; + } + if flags & Q_MODARITH7_FLAG_CONST != 0 { + advance_quotient_cursor(bytes, &mut cursor, 1, op, idx)?; + } + + require_quotient_bytes(bytes, cursor, 5, "MODARITH7 count header")?; + let lin_count = bytes[cursor] as usize; + let row_count = bytes[cursor + 1] as usize; + let pairwise_count = bytes[cursor + 2] as usize; + let mem_count = bytes[cursor + 3] as usize; + let product_count = bytes[cursor + 4] as usize; + advance_quotient_cursor(bytes, &mut cursor, 5, op, idx)?; + + advance_quotient_cursor( + bytes, + &mut cursor, + checked_quotient_len_mul( + lin_count, + QUOTIENT_VM_LIMBS * (1 + QUOTIENT_VM_BYTE_U16_BYTES), + op, + idx, + )?, + op, + idx, + )?; + advance_quotient_cursor( + bytes, + &mut cursor, + checked_quotient_len_mul( + row_count, + QUOTIENT_VM_BYTE_U16_BYTES + QUOTIENT_VM_LIMBS * (1 + QUOTIENT_VM_BYTE_U16_BYTES), + op, + idx, + )?, + op, + idx, + )?; + advance_quotient_cursor( + bytes, + &mut cursor, + checked_quotient_len_mul( + pairwise_count, + 2 * QUOTIENT_VM_BYTE_U16_BYTES + QUOTIENT_VM_PAIRWISE_COEFFS, + op, + idx, + )?, + op, + idx, + )?; + advance_quotient_cursor( + bytes, + &mut cursor, + checked_quotient_len_mul(mem_count, 1 + QUOTIENT_VM_BYTE_U16_BYTES, op, idx)?, + op, + idx, + )?; + advance_quotient_cursor( + bytes, + &mut cursor, + checked_quotient_len_mul(product_count, 1 + 2 * QUOTIENT_VM_BYTE_U16_BYTES, op, idx)?, + op, + idx, + )?; + + Ok(cursor - idx) +} + +/// Apply one opcode's static stack effect during offline validation. +fn apply_quotient_stack_effect( + op: u8, + idx: usize, + depth: &mut usize, + max_stack: &mut usize, +) -> Result<(), String> { + match op { + Q_OP_PUSH_CONST + | Q_OP_PUSH_MEM_LITERAL + | Q_OP_PUSH_MEM_TOKEN + | Q_OP_PUSH_MEM_TOKEN_OFFSET + | Q_OP_PUSH_MEM_U16 + | Q_OP_PUSH_CONST_U8 + | Q_OP_LIN7 + | Q_OP_BILIN7_ROW + | Q_OP_BILIN7_PAIRWISE + | Q_OP_MODARITH7 => { + *depth = depth + .checked_add(1) + .ok_or_else(|| format!("quotient VM stack depth overflow at byte {idx}"))?; + *max_stack = (*max_stack).max(*depth); + } + Q_OP_ADD | Q_OP_MUL => { + require_quotient_stack_depth(op, idx, *depth, 2)?; + *depth -= 1; + } + Q_OP_NEG + | Q_OP_ADD_CONST_U8 + | Q_OP_MUL_CONST_U8 + | Q_OP_ADD_CONST + | Q_OP_MUL_CONST + | Q_OP_ADD_MEM_U16 + | Q_OP_MUL_MEM_U16 + | Q_OP_ADD_MUL_MEM_MEM_CONST_U8 + | Q_OP_ADD_MUL_CONST_U8_MEM_U16 + | Q_OP_ADD_MUL_MEM_MEM + | Q_OP_RUN_ADD_MUL_MEM_MEM_CONST_U8 + | Q_OP_RUN_ADD_MUL_CONST_U8_MEM_U16 + | Q_OP_AFFINE_SUM + | Q_OP_POW5 => { + require_quotient_stack_depth(op, idx, *depth, 1)?; + } + Q_OP_FOLD_MAIN | Q_OP_FOLD_SELECTOR => { + if *depth != 1 { + return Err(format!( + "quotient VM stack boundary error at byte {idx}: opcode {op:#x} requires exactly 1 stack value, found {depth}" + )); + } + *depth = 0; + } + Q_OP_NATIVE_PERMUTATION | Q_OP_NATIVE_LOOKUP | Q_OP_NATIVE_IDENTITY => { + if *depth != 0 { + return Err(format!( + "quotient VM native callback at byte {idx}: opcode {op:#x} requires an empty stack, found {depth}" + )); + } + } + _ => { + return Err(format!( + "unknown quotient VM opcode {op:#x} reached stack validator at byte {idx}" + )); + } + } + Ok(()) +} + +/// Require at least `required` stack values before consuming operands. +fn require_quotient_stack_depth( + op: u8, + idx: usize, + depth: usize, + required: usize, +) -> Result<(), String> { + if depth < required { + return Err(format!( + "quotient VM stack underflow at byte {idx}: opcode {op:#x} requires {required} stack value(s), found {depth}" + )); + } + Ok(()) +} + +/// Reject symbolic memory tokens unknown to this VM version. +fn validate_quotient_mem_token(token: u8, idx: usize) -> Result<(), String> { + if QUOTIENT_VM_SPEC.mem_tokens.iter().any(|spec| spec.token == token) { + Ok(()) + } else { + Err(format!( + "unknown quotient VM memory token {token:#x} at byte {idx}" + )) + } +} + +/// Check that an instruction operand range fits inside the bytecode. +fn require_quotient_bytes( + bytes: &[u8], + idx: usize, + len: usize, + context: &str, +) -> Result<(), String> { + let end = idx + .checked_add(len) + .ok_or_else(|| format!("quotient VM {context} length overflows at byte {idx}"))?; + if end > bytes.len() { + return Err(format!( + "truncated quotient VM {context} at byte {idx}: need {len} byte(s), program has {} byte(s) remaining", + bytes.len().saturating_sub(idx) + )); + } + Ok(()) +} + +/// Advance a dynamic-instruction cursor after bounds-checking the range. +fn advance_quotient_cursor( + bytes: &[u8], + cursor: &mut usize, + amount: usize, + op: u8, + op_idx: usize, +) -> Result<(), String> { + require_quotient_bytes(bytes, *cursor, amount, "dynamic instruction operand")?; + *cursor = cursor.checked_add(amount).ok_or_else(|| { + format!("quotient VM opcode {op:#x} length overflows while decoding byte {op_idx}") + })?; + Ok(()) +} + +/// Checked multiplication for derived instruction lengths. +fn checked_quotient_len_mul(lhs: usize, rhs: usize, op: u8, idx: usize) -> Result { + lhs.checked_mul(rhs).ok_or_else(|| { + format!("quotient VM opcode {op:#x} length multiplication overflows at byte {idx}") + }) +} + +/// Checked addition for derived instruction lengths. +fn checked_quotient_len_add(lhs: usize, rhs: usize, op: u8, idx: usize) -> Result { + lhs.checked_add(rhs).ok_or_else(|| { + format!("quotient VM opcode {op:#x} length addition overflows at byte {idx}") + }) +} + +/// Read a big-endian `u16` operand from bytecode. +pub(crate) fn read_u16(bytes: &[u8], idx: usize) -> u16 { + u16::from_be_bytes(bytes[idx..idx + 2].try_into().expect("u16 quotient operand")) +} + +/// Read a big-endian `u32` operand from bytecode. +#[cfg(test)] +pub(crate) fn read_u32(bytes: &[u8], idx: usize) -> u32 { + u32::from_be_bytes(bytes[idx..idx + 4].try_into().expect("u32 quotient operand")) +} + +/// Canonical key for structural expression matching and deduplication. +/// +/// Addition and multiplication are keyed commutatively because all arithmetic +/// is over Fr and these rewrites do not alter evaluation order side effects: +/// `QuotientExpr` has no side effects. +pub(crate) fn quotient_expr_key(expr: &QuotientExpr) -> String { + match expr { + QuotientExpr::Const(value) => format!("c:{value:x}"), + QuotientExpr::Mem(QuotientMem::Literal(ptr)) => format!("m:{ptr:x}"), + QuotientExpr::Mem(QuotientMem::Token(token)) => format!("t:{token:x}"), + QuotientExpr::Mem(QuotientMem::TokenOffset(token, offset)) => { + format!("to:{token:x}:{offset:x}") + } + QuotientExpr::Add(lhs, rhs) => quotient_commutative_expr_key("a", lhs, rhs), + QuotientExpr::Mul(lhs, rhs) => quotient_commutative_expr_key("u", lhs, rhs), + QuotientExpr::Neg(inner) => format!("n:{}", quotient_expr_key(inner)), + } +} + +/// Resolve a generated Yul memory symbol name to its compact token. +pub(crate) fn quotient_mem_token_from_name(name: &str) -> Option { + QUOTIENT_MEM_TOKEN_TABLE + .iter() + .find_map(|spec| (spec.name == name).then_some(spec.token)) +} + +/// Environment needed to lower Halo2 `Expression` leaves. +/// +/// The VM lowerer is independent of the concrete verifier memory layout; this +/// trait supplies the memory-backed expression for each kind of query. +pub(crate) trait QuotientExpressionEnv { + /// Lower a selector leaf. + fn selector(&self, selector: Selector) -> QuotientExpr; + /// Lower a fixed-column query leaf. + fn fixed(&self, column_index: usize, rotation: i32) -> QuotientExpr; + /// Lower an advice-column query leaf. + fn advice(&self, column_index: usize, rotation: i32) -> QuotientExpr; + /// Lower an instance-column query leaf. + fn instance(&self, column_index: usize, rotation: i32) -> QuotientExpr; + /// Lower a challenge leaf. + fn challenge(&self, index: usize) -> QuotientExpr; +} + +/// Lower a Halo2 expression into the compact quotient AST. +pub(crate) fn quotient_expr_from_expression( + env: &E, + expression: &Expression, +) -> QuotientExpr { + expression.evaluate( + &|scalar| QuotientExpr::Const(fe_to_u256::(&scalar)), + &|selector| env.selector(selector), + &|query| env.fixed(query.column_index(), query.rotation().0), + &|query| env.advice(query.column_index(), query.rotation().0), + &|query| env.instance(query.column_index(), query.rotation().0), + &|challenge| env.challenge(challenge.index()), + &|inner| QuotientExpr::Neg(Box::new(inner)), + &|lhs, rhs| QuotientExpr::Add(Box::new(lhs), Box::new(rhs)), + &|lhs, rhs| QuotientExpr::Mul(Box::new(lhs), Box::new(rhs)), + &|inner, scalar| { + QuotientExpr::Mul( + Box::new(inner), + Box::new(QuotientExpr::Const(fe_to_u256::(&scalar))), + ) + }, + ) +} + +/// Production expression environment backed by generated verifier data. +pub(crate) struct DataQuotientExpressionEnv<'a> { + /// Constraint-system metadata used to classify selectors and instances. + pub(crate) meta: &'a ConstraintSystemMeta, + /// Concrete generated memory locations for proof/VK/challenge values. + pub(crate) data: &'a Data, +} + +impl QuotientExpressionEnv for DataQuotientExpressionEnv<'_> { + /// Reject virtual selectors after selector-to-fixed conversion. + fn selector(&self, _selector: Selector) -> QuotientExpr { + panic!("virtual selectors must be removed before quotient lowering") + } + + /// Lower a fixed query, synthesizing simple selectors as constant one. + fn fixed(&self, column_index: usize, rotation: i32) -> QuotientExpr { + if self.meta.simple_selector_cols.contains(&column_index) { + QuotientExpr::Const(U256::from(1u64)) + } else { + word_to_quotient_expr( + *self + .data + .fixed_evals + .get(&(column_index, rotation)) + .expect("fixed eval present"), + ) + } + } + + /// Lower an advice query to a memory-backed eval word. + fn advice(&self, column_index: usize, rotation: i32) -> QuotientExpr { + word_to_quotient_expr( + *self + .data + .advice_evals + .get(&(column_index, rotation)) + .expect("advice eval present"), + ) + } + + /// Lower an instance query from proof evals or local instance evaluation. + fn instance(&self, column_index: usize, rotation: i32) -> QuotientExpr { + if column_index < self.meta.num_committed_instances { + word_to_quotient_expr( + *self + .data + .committed_instance_evals + .get(&(column_index, rotation)) + .expect("committed instance eval present"), + ) + } else { + word_to_quotient_expr(self.data.instance_eval) + } + } + + /// Lower a user challenge to its memory-backed word. + fn challenge(&self, index: usize) -> QuotientExpr { + word_to_quotient_expr(self.data.challenges[index]) + } +} + +/// Convert a memory-backed `Word` into a quotient memory load expression. +pub(crate) fn word_to_quotient_expr(word: Word) -> QuotientExpr { + assert_eq!( + word.loc(), + Location::Memory, + "quotient expressions can only load memory-backed words" + ); + QuotientExpr::Mem(ptr_to_quotient_mem(word.ptr())) +} + +/// Convert a generated memory pointer into the compact VM pointer form. +pub(crate) fn ptr_to_quotient_mem(ptr: Ptr) -> QuotientMem { + assert_eq!( + ptr.loc(), + Location::Memory, + "quotient expressions can only load memory-backed words" + ); + match ptr.value() { + Value::Integer(offset) => { + assert!(offset >= 0, "negative quotient memory pointer"); + QuotientMem::Literal(offset as u32) + } + Value::Identifier(name, offset) => { + assert!(offset >= 0, "negative quotient memory token offset"); + let token = quotient_mem_token_from_name(name) + .unwrap_or_else(|| panic!("unsupported quotient memory token: {name}")); + if offset == 0 { + QuotientMem::Token(token) + } else { + QuotientMem::TokenOffset(token, offset as u32) + } + } + } +} + +/// Canonical key helper for commutative binary operations. +pub(crate) fn quotient_commutative_expr_key( + op: &str, + lhs: &QuotientExpr, + rhs: &QuotientExpr, +) -> String { + let lhs = quotient_expr_key(lhs); + let rhs = quotient_expr_key(rhs); + if lhs <= rhs { + format!("{op}:{lhs}:{rhs}") + } else { + format!("{op}:{rhs}:{lhs}") + } +} + +/// Byte length of the opcode at `idx`. +/// +/// Fixed-width opcodes are read from the spec table; byte-only run and +/// MODARITH7 opcodes decode their embedded counts. +pub(crate) fn quotient_op_len(bytes: &[u8], idx: usize) -> usize { + let op = bytes[idx]; + match op { + Q_OP_RUN_ADD_MUL_MEM_MEM_CONST_U8 => { + 1 + QUOTIENT_VM_BYTE_U16_BYTES + + (read_u16(bytes, idx + 1) as usize) * (2 * QUOTIENT_VM_BYTE_U16_BYTES + 1) + } + Q_OP_RUN_ADD_MUL_CONST_U8_MEM_U16 => { + 1 + QUOTIENT_VM_BYTE_U16_BYTES + + (read_u16(bytes, idx + 1) as usize) * (QUOTIENT_VM_BYTE_U16_BYTES + 1) + } + Q_OP_AFFINE_SUM => quotient_affine_sum_op_len(bytes, idx), + Q_OP_MODARITH7 => quotient_modarith7_op_len(bytes, idx), + _ => quotient_opcode_byte_len(op) + .unwrap_or_else(|| panic!("unknown quotient op {op:#x} at byte {idx}")), + } +} + +/// Decode `AFFINE_SUM` length in trusted/internal bytecode walks. +fn quotient_affine_sum_op_len(bytes: &[u8], idx: usize) -> usize { + let lin_count = read_u16(bytes, idx + 1) as usize; + let product_count = read_u16(bytes, idx + 3) as usize; + 1 + 2 * QUOTIENT_VM_BYTE_U16_BYTES + + lin_count * (QUOTIENT_VM_BYTE_U16_BYTES + 1) + + product_count * (2 * QUOTIENT_VM_BYTE_U16_BYTES + 1) +} + +/// Decode `MODARITH7` length in trusted/internal bytecode walks. +fn quotient_modarith7_op_len(bytes: &[u8], idx: usize) -> usize { + let mut cursor = idx + 1; + let flags = bytes[cursor]; + cursor += 1; + if flags & Q_MODARITH7_FLAG_COND != 0 { + cursor += QUOTIENT_VM_BYTE_U16_BYTES; + } + if flags & Q_MODARITH7_FLAG_CONST != 0 { + cursor += 1; + } + let lin_count = bytes[cursor] as usize; + let row_count = bytes[cursor + 1] as usize; + let pairwise_count = bytes[cursor + 2] as usize; + let mem_count = bytes[cursor + 3] as usize; + let product_count = bytes[cursor + 4] as usize; + cursor += 5; + cursor += lin_count * QUOTIENT_VM_LIMBS * (1 + QUOTIENT_VM_BYTE_U16_BYTES); + cursor += row_count + * (QUOTIENT_VM_BYTE_U16_BYTES + QUOTIENT_VM_LIMBS * (1 + QUOTIENT_VM_BYTE_U16_BYTES)); + cursor += pairwise_count * (2 * QUOTIENT_VM_BYTE_U16_BYTES + QUOTIENT_VM_PAIRWISE_COEFFS); + cursor += mem_count * (1 + QUOTIENT_VM_BYTE_U16_BYTES); + cursor += product_count * (1 + 2 * QUOTIENT_VM_BYTE_U16_BYTES); + cursor - idx +} + +/// Iterate over decoded byte-oriented quotient instructions. +/// +/// This is intentionally lightweight: safety validation still lives in +/// `validate_quotient_program`, while codegen-side metrics can share the same +/// offset walk instead of open-coding counters. +pub(crate) fn quotient_bytecode_ops(bytes: &[u8]) -> QuotientBytecodeOps<'_> { + QuotientBytecodeOps { bytes, idx: 0 } +} + +/// Iterator over `(offset, opcode, byte_len)` triples for a finalized program. +pub(crate) struct QuotientBytecodeOps<'a> { + bytes: &'a [u8], + idx: usize, +} + +impl Iterator for QuotientBytecodeOps<'_> { + /// Offset, opcode byte, and decoded byte length. + type Item = (usize, u8, usize); + + /// Return the next decoded opcode span. + fn next(&mut self) -> Option { + (self.idx < self.bytes.len()).then(|| { + let idx = self.idx; + let op = self.bytes[idx]; + let len = quotient_op_len(self.bytes, idx); + self.idx += len; + (idx, op, len) + }) + } +} + +/// Return the leaf form of an expression, if it has no arithmetic children. +pub(crate) fn quotient_leaf(expr: &QuotientExpr) -> Option { + match expr { + QuotientExpr::Const(value) => Some(QuotientLeaf::Const(*value)), + QuotientExpr::Mem(mem) => Some(QuotientLeaf::Mem(*mem)), + QuotientExpr::Add(_, _) | QuotientExpr::Mul(_, _) | QuotientExpr::Neg(_) => None, + } +} + +/// Flatten a pure multiplication tree into constant/memory leaves. +/// +/// This intentionally rejects sums and negations. Additive structure is handled +/// by sum collection or by the generic emitter; fused product-add opcodes need +/// a product that can be encoded directly. +pub(crate) fn collect_product_leaves(expr: &QuotientExpr, leaves: &mut Vec) -> bool { + match expr { + QuotientExpr::Mul(lhs, rhs) => { + collect_product_leaves(lhs, leaves) && collect_product_leaves(rhs, leaves) + } + QuotientExpr::Const(_) | QuotientExpr::Mem(_) => { + if let Some(leaf) = quotient_leaf(expr) { + leaves.push(leaf); + true + } else { + false + } + } + QuotientExpr::Add(_, _) | QuotientExpr::Neg(_) => false, + } +} + +/// Flatten multiplication nodes while keeping additive subexpressions whole. +fn collect_product_expr_factors<'a>(expr: &'a QuotientExpr, factors: &mut Vec<&'a QuotientExpr>) { + match expr { + QuotientExpr::Mul(lhs, rhs) => { + collect_product_expr_factors(lhs, factors); + collect_product_expr_factors(rhs, factors); + } + QuotientExpr::Const(_) + | QuotientExpr::Mem(_) + | QuotientExpr::Add(_, _) + | QuotientExpr::Neg(_) => { + factors.push(expr); + } + } +} + +/// Construct an addition node without applying algebraic simplification. +fn quotient_sum_expr(lhs: QuotientExpr, rhs: QuotientExpr) -> QuotientExpr { + QuotientExpr::Add(Box::new(lhs), Box::new(rhs)) +} + +/// Attach a scalar coefficient to a term, eliding multiplication by one. +fn quotient_scaled_term_expr(coeff: Fq, expr: QuotientExpr) -> QuotientExpr { + if coeff == Fq::ONE { + expr + } else { + QuotientExpr::Mul( + Box::new(QuotientExpr::Const(quotient_fq_to_u256(coeff))), + Box::new(expr), + ) + } +} + +/// Recognize any supported seven-limb foreign-field quotient shape. +pub(crate) fn quotient_limb_shape(expr: &QuotientExpr) -> Option { + // Recover foreign-field limb algebra from the generic `QuotientExpr` + // tree. This deliberately does not look at gate names: the Rust verifier + // source of truth remains + // proofs/src/plonk/mod.rs::partially_evaluate_identities, which evaluates + // `vk.cs.gates` expression trees in order. The recognizer only changes how + // obvious `sum_exprs` / `pair_wise_prod` shapes are encoded for Solidity. + let mut terms = Vec::new(); + if !collect_quotient_sum_terms(expr, Fq::ONE, &mut terms) { + return None; + } + terms.retain(|(coeff, _)| *coeff != Fq::ZERO); + + try_quotient_bilin7_pairwise_shape(&terms) + .or_else(|| try_quotient_bilin7_row_shape(&terms)) + .or_else(|| try_quotient_lin7_shape(&terms)) +} + +/// Return the repeated base for a structural fifth power. +pub(crate) fn quotient_pow5_base(expr: &QuotientExpr) -> Option<&QuotientExpr> { + let mut factors = Vec::with_capacity(5); + collect_product_expr_factors(expr, &mut factors); + if factors.len() != 5 { + return None; + } + let first_key = quotient_expr_key(factors[0]); + factors + .iter() + .all(|factor| quotient_expr_key(factor) == first_key) + .then_some(factors[0]) +} + +/// Extract one limb shape from a larger affine sum and return the residue. +pub(crate) fn quotient_limb_subshape( + expr: &QuotientExpr, +) -> Option<(QuotientLimbShape, QuotientExpr)> { + let mut terms = Vec::new(); + let mut constant = Fq::ZERO; + if !collect_quotient_affine_terms(expr, Fq::ONE, &mut terms, &mut constant) { + return None; + } + terms.retain(|(coeff, _)| *coeff != Fq::ZERO); + if terms.len() <= QUOTIENT_VM_LIMBS && constant == Fq::ZERO { + return None; + } + + let (shape, used) = try_quotient_bilin7_pairwise_subshape(&terms) + .or_else(|| try_quotient_bilin7_row_subshape(&terms)) + .or_else(|| try_quotient_lin7_subshape(&terms))?; + if used.is_empty() || (used.len() == terms.len() && constant == Fq::ZERO) { + return None; + } + + let used = used.into_iter().collect::>(); + let mut residue = QuotientExpr::Const(quotient_fq_to_u256(constant)); + for (idx, (coeff, term)) in terms.into_iter().enumerate() { + if used.contains(&idx) { + continue; + } + residue = quotient_sum_expr(residue, quotient_scaled_term_expr(coeff, (*term).clone())); + } + Some((shape, residue)) +} + +/// Recognize a whole affine foreign-field/ECC identity that can be evaluated +/// by the dynamic `MODARITH7` opcode. +pub(crate) fn quotient_modarith7_shape(expr: &QuotientExpr) -> Option { + if let Some((cond, inner)) = quotient_condition_factor(expr) { + if let Some(shape) = quotient_modarith7_affine_shape(Some(cond), &inner) { + return Some(shape); + } + } + quotient_modarith7_affine_shape(None, expr) +} + +/// Split `cond * inner` when `cond` is a literal memory load, moving any scalar +/// factor attached to `cond` into `inner`. +fn quotient_condition_factor(expr: &QuotientExpr) -> Option<(u16, QuotientExpr)> { + let QuotientExpr::Mul(lhs, rhs) = expr else { + return None; + }; + + if let Some((coeff, ptr)) = quotient_mem_term(lhs) { + return Some((ptr, quotient_scaled_term_expr(coeff, rhs.as_ref().clone()))); + } + if let Some((coeff, ptr)) = quotient_mem_term(rhs) { + return Some((ptr, quotient_scaled_term_expr(coeff, lhs.as_ref().clone()))); + } + None +} + +/// Recognize a mixed affine form after any outer condition factor has been +/// separated. +fn quotient_modarith7_affine_shape( + cond: Option, + expr: &QuotientExpr, +) -> Option { + let mut terms = Vec::new(); + let mut constant = Fq::ZERO; + if !collect_quotient_affine_terms(expr, Fq::ONE, &mut terms, &mut constant) { + return None; + } + terms.retain(|(coeff, _)| *coeff != Fq::ZERO); + + let mut lin = Vec::new(); + let mut rows = Vec::new(); + let mut pairwise = Vec::new(); + + loop { + if let Some((shape, used)) = try_quotient_bilin7_pairwise_subshape(&terms) { + if used.is_empty() { + break; + } + if let QuotientLimbShape::Bilin7Pairwise { + lhs_base, + rhs_base, + coeffs, + } = shape + { + pairwise.push((lhs_base, rhs_base, coeffs)); + } else { + unreachable!("pairwise recognizer returned non-pairwise shape"); + } + remove_quotient_terms(&mut terms, &used); + continue; + } + if let Some((shape, used)) = try_quotient_factored_bilin7_row_subshape(&terms) { + if used.is_empty() { + break; + } + if let QuotientLimbShape::Bilin7Row { lhs, terms } = shape { + rows.push((lhs, terms)); + } else { + unreachable!("factored row recognizer returned non-row shape"); + } + remove_quotient_terms(&mut terms, &used); + continue; + } + if let Some((shape, used)) = try_quotient_bilin7_row_subshape(&terms) { + if used.is_empty() { + break; + } + if let QuotientLimbShape::Bilin7Row { lhs, terms } = shape { + rows.push((lhs, terms)); + } else { + unreachable!("row recognizer returned non-row shape"); + } + remove_quotient_terms(&mut terms, &used); + continue; + } + if let Some((shape, used)) = try_quotient_lin7_subshape(&terms) { + if used.is_empty() { + break; + } + if let QuotientLimbShape::Lin7 { terms } = shape { + lin.push(terms); + } else { + unreachable!("linear recognizer returned non-linear shape"); + } + remove_quotient_terms(&mut terms, &used); + continue; + } + break; + } + + if lin.len() > u8::MAX as usize + || rows.len() > u8::MAX as usize + || pairwise.len() > u8::MAX as usize + { + return None; + } + + let mut grouped_mem = Vec::<(u16, Fq)>::new(); + let mut grouped_products = Vec::<(u16, u16, Fq)>::new(); + for (coeff, term) in terms { + if let Some((inner_coeff, ptr)) = quotient_mem_term(term) { + add_grouped_limb_coeff(&mut grouped_mem, ptr, coeff * inner_coeff); + } else if let Some((inner_coeff, lhs, rhs)) = quotient_product_mem_pair(term) { + add_grouped_product_coeff(&mut grouped_products, lhs, rhs, coeff * inner_coeff); + } else if add_factored_affine_product_terms( + &mut grouped_mem, + &mut grouped_products, + coeff, + term, + ) + .is_none() + { + return None; + } + } + grouped_mem.retain(|(_, coeff)| *coeff != Fq::ZERO); + grouped_mem.sort_by_key(|(ptr, _)| *ptr); + grouped_products.retain(|(_, _, coeff)| *coeff != Fq::ZERO); + grouped_products.sort_by_key(|(lhs, rhs, _)| (*lhs, *rhs)); + if grouped_mem.len() > u8::MAX as usize || grouped_products.len() > u8::MAX as usize { + return None; + } + + let nonzero_constant = usize::from(constant != Fq::ZERO); + let component_count = lin.len() + + rows.len() + + pairwise.len() + + grouped_mem.len() + + grouped_products.len() + + nonzero_constant; + if component_count == 0 { + return None; + } + let has_limb_component = !lin.is_empty() || !rows.is_empty() || !pairwise.is_empty(); + let has_sparse_conditional_products = + cond.is_some() && !grouped_products.is_empty() && component_count >= 2; + if !has_limb_component && !has_sparse_conditional_products { + return None; + } + if cond.is_none() && component_count < 2 { + return None; + } + + Some(QuotientModarith7Shape { + cond, + constant: quotient_fq_to_u256(constant), + lin, + rows, + pairwise, + mem_terms: grouped_mem + .into_iter() + .map(|(ptr, coeff)| (quotient_fq_to_u256(coeff), ptr)) + .collect(), + product_terms: grouped_products + .into_iter() + .map(|(lhs, rhs, coeff)| (quotient_fq_to_u256(coeff), lhs, rhs)) + .collect(), + }) +} + +/// Remove recognized term indices from an affine term list. +fn remove_quotient_terms(terms: &mut Vec<(Fq, &QuotientExpr)>, used: &[usize]) { + let used = used.iter().copied().collect::>(); + let mut idx = 0usize; + terms.retain(|_| { + let keep = !used.contains(&idx); + idx += 1; + keep + }); +} + +/// Add a coefficient to an unordered memory-product pair. +fn add_grouped_product_coeff(grouped: &mut Vec<(u16, u16, Fq)>, lhs: u16, rhs: u16, coeff: Fq) { + let (lhs, rhs) = if lhs <= rhs { (lhs, rhs) } else { (rhs, lhs) }; + if let Some((_, _, existing)) = grouped + .iter_mut() + .find(|(existing_lhs, existing_rhs, _)| *existing_lhs == lhs && *existing_rhs == rhs) + { + *existing += coeff; + } else { + grouped.push((lhs, rhs, coeff)); + } +} + +/// Recognize `mem * (affine mem sum)` and fold it into sparse product/memory +/// buckets for `MODARITH7`. +fn add_factored_affine_product_terms( + grouped_mem: &mut Vec<(u16, Fq)>, + grouped_products: &mut Vec<(u16, u16, Fq)>, + term_coeff: Fq, + expr: &QuotientExpr, +) -> Option<()> { + let mut factors = Vec::new(); + collect_product_expr_factors(expr, &mut factors); + if factors.len() < 2 { + return None; + } + + let mut coeff = term_coeff; + let mut lhs = None; + let mut affine_expr = None; + for factor in factors { + match factor { + QuotientExpr::Const(value) => coeff *= quotient_fq_from_u256(*value)?, + QuotientExpr::Mem(QuotientMem::Literal(ptr)) => { + if lhs.replace(u16::try_from(*ptr).ok()?).is_some() { + return None; + } + } + QuotientExpr::Mem(QuotientMem::Token(_)) + | QuotientExpr::Mem(QuotientMem::TokenOffset(_, _)) => return None, + QuotientExpr::Add(_, _) | QuotientExpr::Mul(_, _) | QuotientExpr::Neg(_) => { + if affine_expr.replace(factor).is_some() { + return None; + } + } + } + } + + let lhs = lhs?; + let affine_expr = affine_expr?; + let mut terms = Vec::new(); + let mut constant = Fq::ZERO; + if !collect_quotient_affine_terms(affine_expr, Fq::ONE, &mut terms, &mut constant) { + return None; + } + if constant != Fq::ZERO { + add_grouped_limb_coeff(grouped_mem, lhs, coeff * constant); + } + for (inner_coeff, term) in terms { + let (mem_coeff, rhs) = quotient_mem_term(term)?; + add_grouped_product_coeff(grouped_products, lhs, rhs, coeff * inner_coeff * mem_coeff); + } + Some(()) +} + +/// Find one factored row-shaped limb term inside a larger affine sum. +fn try_quotient_factored_bilin7_row_subshape( + terms: &[(Fq, &QuotientExpr)], +) -> Option<(QuotientLimbShape, Vec)> { + for (idx, (coeff, expr)) in terms.iter().enumerate() { + if let Some(shape) = quotient_factored_bilin7_row_shape(*coeff, expr) { + return Some((shape, vec![idx])); + } + } + None +} + +/// Recognize `lhs_limb * lin7(rhs_limbs)` as a row bilinear limb shape. +fn quotient_factored_bilin7_row_shape( + term_coeff: Fq, + expr: &QuotientExpr, +) -> Option { + let mut factors = Vec::new(); + collect_product_expr_factors(expr, &mut factors); + if factors.len() < 2 { + return None; + } + + let mut coeff = term_coeff; + let mut lhs = None; + let mut lin_expr = None; + for factor in factors { + match factor { + QuotientExpr::Const(value) => coeff *= quotient_fq_from_u256(*value)?, + QuotientExpr::Mem(QuotientMem::Literal(ptr)) => { + if lhs.replace(u16::try_from(*ptr).ok()?).is_some() { + return None; + } + } + QuotientExpr::Mem(QuotientMem::Token(_)) + | QuotientExpr::Mem(QuotientMem::TokenOffset(_, _)) => return None, + QuotientExpr::Add(_, _) | QuotientExpr::Mul(_, _) | QuotientExpr::Neg(_) => { + if lin_expr.replace(factor).is_some() { + return None; + } + } + } + } + + let lhs = lhs?; + let lin_expr = lin_expr?; + let mut lin_terms = Vec::new(); + if !collect_quotient_sum_terms(lin_expr, coeff, &mut lin_terms) { + return None; + } + match try_quotient_lin7_shape(&lin_terms)? { + QuotientLimbShape::Lin7 { terms } => Some(QuotientLimbShape::Bilin7Row { lhs, terms }), + QuotientLimbShape::Bilin7Row { .. } | QuotientLimbShape::Bilin7Pairwise { .. } => None, + } +} + +/// Return constant-table coefficients referenced by a `MODARITH7` shape. +fn modarith7_coeffs(shape: &QuotientModarith7Shape) -> Vec { + let mut coeffs = Vec::new(); + if shape.constant != U256::ZERO { + coeffs.push(shape.constant); + } + for terms in &shape.lin { + coeffs.extend(terms.iter().map(|(coeff, _)| *coeff)); + } + for (_, terms) in &shape.rows { + coeffs.extend(terms.iter().map(|(coeff, _)| *coeff)); + } + for (_, _, terms) in &shape.pairwise { + coeffs.extend(terms.iter().copied()); + } + coeffs.extend(shape.mem_terms.iter().map(|(coeff, _)| *coeff)); + coeffs.extend(shape.product_terms.iter().map(|(coeff, _, _)| *coeff)); + coeffs +} + +/// Collect additive terms for subshape extraction, preserving constants as the +/// residue instead of rejecting the whole affine identity. +fn collect_quotient_affine_terms<'a>( + expr: &'a QuotientExpr, + coeff: Fq, + terms: &mut Vec<(Fq, &'a QuotientExpr)>, + constant: &mut Fq, +) -> bool { + if coeff == Fq::ZERO { + return true; + } + + match expr { + QuotientExpr::Add(lhs, rhs) => { + collect_quotient_affine_terms(lhs, coeff, terms, constant) + && collect_quotient_affine_terms(rhs, coeff, terms, constant) + } + QuotientExpr::Neg(inner) => collect_quotient_affine_terms(inner, -coeff, terms, constant), + QuotientExpr::Mul(lhs, rhs) => { + if let QuotientExpr::Const(value) = lhs.as_ref() { + let Some(value) = quotient_fq_from_u256(*value) else { + return false; + }; + collect_quotient_affine_terms(rhs, coeff * value, terms, constant) + } else if let QuotientExpr::Const(value) = rhs.as_ref() { + let Some(value) = quotient_fq_from_u256(*value) else { + return false; + }; + collect_quotient_affine_terms(lhs, coeff * value, terms, constant) + } else { + terms.push((coeff, expr)); + true + } + } + QuotientExpr::Const(value) => { + let Some(value) = quotient_fq_from_u256(*value) else { + return false; + }; + *constant += coeff * value; + true + } + QuotientExpr::Mem(_) => { + terms.push((coeff, expr)); + true + } + } +} + +/// Collect additive terms as `(coefficient, expression)` pairs over Fr. +/// +/// Constant-only terms must reduce to zero for a limb shape to match; non-zero +/// standalone constants would require an additional VM add and therefore are +/// left to the generic emitter. +pub(crate) fn collect_quotient_sum_terms<'a>( + expr: &'a QuotientExpr, + coeff: Fq, + terms: &mut Vec<(Fq, &'a QuotientExpr)>, +) -> bool { + if coeff == Fq::ZERO { + return true; + } + + match expr { + QuotientExpr::Add(lhs, rhs) => { + collect_quotient_sum_terms(lhs, coeff, terms) + && collect_quotient_sum_terms(rhs, coeff, terms) + } + QuotientExpr::Neg(inner) => collect_quotient_sum_terms(inner, -coeff, terms), + QuotientExpr::Mul(lhs, rhs) => { + if let QuotientExpr::Const(value) = lhs.as_ref() { + let Some(value) = quotient_fq_from_u256(*value) else { + return false; + }; + collect_quotient_sum_terms(rhs, coeff * value, terms) + } else if let QuotientExpr::Const(value) = rhs.as_ref() { + let Some(value) = quotient_fq_from_u256(*value) else { + return false; + }; + collect_quotient_sum_terms(lhs, coeff * value, terms) + } else { + terms.push((coeff, expr)); + true + } + } + QuotientExpr::Const(value) => { + let Some(value) = quotient_fq_from_u256(*value) else { + return false; + }; + coeff * value == Fq::ZERO + } + QuotientExpr::Mem(_) => { + terms.push((coeff, expr)); + true + } + } +} + +/// Recognize a seven-limb linear combination. +/// +/// The resulting terms are sorted by memory pointer so equivalent expressions +/// have deterministic bytecode even when the source expression tree orders +/// additions differently. +pub(crate) fn try_quotient_lin7_shape(terms: &[(Fq, &QuotientExpr)]) -> Option { + // Matches: + // circuits/src/field/foreign/gates/norm.rs + // sum_exprs(base_powers, shifted_x) - sum_exprs(base_powers, zs) + // circuits/src/field/foreign/gates/mul.rs + // sum_exprs(base_powers, xs/ys/zs) + // and the same base-power sums reused by ECC foreign gates. + let mut grouped = Vec::<(u16, Fq)>::new(); + for (coeff, expr) in terms { + let (inner_coeff, ptr) = quotient_mem_term(expr)?; + add_grouped_limb_coeff(&mut grouped, ptr, *coeff * inner_coeff); + } + grouped.retain(|(_, coeff)| *coeff != Fq::ZERO); + if grouped.len() != QUOTIENT_VM_LIMBS { + return None; + } + grouped.sort_by_key(|(ptr, _)| *ptr); + Some(QuotientLimbShape::Lin7 { + terms: grouped + .into_iter() + .map(|(ptr, coeff)| (quotient_fq_to_u256(coeff), ptr)) + .collect(), + }) +} + +/// Recognize one limb multiplied across a seven-limb vector. +/// +/// The recognizer tries both sides of the first pair as the repeated limb so it +/// is insensitive to multiplication commutativity in the lowered expression. +pub(crate) fn try_quotient_bilin7_row_shape( + terms: &[(Fq, &QuotientExpr)], +) -> Option { + // Matches one fixed limb multiplied across a 7-limb vector. This is a + // local slice of the `pair_wise_prod` formulas in: + // circuits/src/field/foreign/gates/mul.rs + // circuits/src/ecc/foreign/gates/{on_curve,slope,tangent,lambda_squared}.rs + let mut pairs = Vec::with_capacity(terms.len()); + for (coeff, expr) in terms { + let (inner_coeff, lhs, rhs) = quotient_product_mem_pair(expr)?; + pairs.push((*coeff * inner_coeff, lhs, rhs)); + } + if pairs.len() != QUOTIENT_VM_LIMBS { + return None; + } + + for candidate in [pairs[0].1, pairs[0].2] { + let mut grouped = Vec::<(u16, Fq)>::new(); + let mut ok = true; + for (coeff, lhs, rhs) in &pairs { + let other = if *lhs == candidate { + *rhs + } else if *rhs == candidate { + *lhs + } else { + ok = false; + break; + }; + add_grouped_limb_coeff(&mut grouped, other, *coeff); + } + grouped.retain(|(_, coeff)| *coeff != Fq::ZERO); + if ok && grouped.len() == QUOTIENT_VM_LIMBS { + grouped.sort_by_key(|(ptr, _)| *ptr); + return Some(QuotientLimbShape::Bilin7Row { + lhs: candidate, + terms: grouped + .into_iter() + .map(|(ptr, coeff)| (quotient_fq_to_u256(coeff), ptr)) + .collect(), + }); + } + } + + None +} + +/// Recognize a full seven-by-seven foreign-field product convolution. +/// +/// The memory pointers must contain two contiguous seven-word limb vectors. For +/// each product term the coefficient is accumulated into its row-major slot and +/// then checked to depend only on `i + j`, exactly matching the +/// `double_base_powers` shape. If any slot is missing or coefficients disagree, +/// the shape is rejected and the generic VM remains the source of truth. +pub(crate) fn try_quotient_bilin7_pairwise_shape( + terms: &[(Fq, &QuotientExpr)], +) -> Option { + // Matches the full foreign-field product convolution: + // sum_exprs(double_base_powers, pair_wise_prod(lhs, rhs)) + // where double_base_powers[k] = base^k mod m. The Rust helper + // pair_wise_prod emits 49 terms in row-major order; after collection we + // require coefficients with the same i+j to agree, exactly the + // base^(i+j) pattern documented in foreign/params.rs. + let mut pairs = Vec::with_capacity(terms.len()); + let mut ptrs = HashSet::new(); + for (coeff, expr) in terms { + let (inner_coeff, lhs, rhs) = quotient_product_mem_pair(expr)?; + pairs.push((*coeff * inner_coeff, lhs, rhs)); + ptrs.insert(lhs); + ptrs.insert(rhs); + } + if pairs.len() != QUOTIENT_VM_PAIRWISE_TERMS { + return None; + } + + let bases = limb7_base_candidates(&ptrs); + for lhs_base in &bases { + for rhs_base in &bases { + let mut coeffs = vec![Fq::ZERO; QUOTIENT_VM_PAIRWISE_TERMS]; + let mut seen = [false; QUOTIENT_VM_PAIRWISE_TERMS]; + let mut ok = true; + + for (coeff, lhs, rhs) in &pairs { + let direct = limb7_index(*lhs_base, *lhs).zip(limb7_index(*rhs_base, *rhs)); + let swapped = limb7_index(*lhs_base, *rhs).zip(limb7_index(*rhs_base, *lhs)); + let Some((i, j)) = direct.or(swapped) else { + ok = false; + break; + }; + let idx = i * QUOTIENT_VM_LIMBS + j; + coeffs[idx] += *coeff; + seen[idx] = true; + } + + if !ok || seen.iter().any(|seen| !seen) { + continue; + } + + let mut by_sum = vec![None; QUOTIENT_VM_PAIRWISE_COEFFS]; + for i in 0..QUOTIENT_VM_LIMBS { + for j in 0..QUOTIENT_VM_LIMBS { + let coeff = coeffs[i * QUOTIENT_VM_LIMBS + j]; + let slot = &mut by_sum[i + j]; + if let Some(expected) = slot { + if *expected != coeff { + ok = false; + break; + } + } else { + *slot = Some(coeff); + } + } + if !ok { + break; + } + } + + if ok { + return Some(QuotientLimbShape::Bilin7Pairwise { + lhs_base: *lhs_base, + rhs_base: *rhs_base, + coeffs: by_sum + .into_iter() + .map(|coeff| quotient_fq_to_u256(coeff.expect("pairwise sum coefficient"))) + .collect(), + }); + } + } + } + + None +} + +/// Find a complete seven-limb linear subshape inside a larger affine sum. +fn try_quotient_lin7_subshape( + terms: &[(Fq, &QuotientExpr)], +) -> Option<(QuotientLimbShape, Vec)> { + let mut mem_terms = Vec::<(usize, u16, Fq)>::new(); + for (idx, (coeff, expr)) in terms.iter().enumerate() { + if let Some((inner_coeff, ptr)) = quotient_mem_term(expr) { + mem_terms.push((idx, ptr, *coeff * inner_coeff)); + } + } + + let ptrs = mem_terms.iter().map(|(_, ptr, _)| *ptr).collect::>(); + for base in limb7_base_candidates(&ptrs) { + let mut grouped = Vec::<(u16, Fq)>::new(); + let mut used = Vec::with_capacity(QUOTIENT_VM_LIMBS); + for limb in 0..QUOTIENT_VM_LIMBS { + let ptr = base + (limb as u16) * WORD_BYTES as u16; + let mut coeff = Fq::ZERO; + let mut found = false; + for (idx, term_ptr, term_coeff) in &mem_terms { + if *term_ptr == ptr { + coeff += *term_coeff; + used.push(*idx); + found = true; + } + } + if !found { + used.clear(); + break; + } + grouped.push((ptr, coeff)); + } + grouped.retain(|(_, coeff)| *coeff != Fq::ZERO); + if grouped.len() == QUOTIENT_VM_LIMBS && used.len() >= QUOTIENT_VM_LIMBS { + grouped.sort_by_key(|(ptr, _)| *ptr); + return Some(( + QuotientLimbShape::Lin7 { + terms: grouped + .into_iter() + .map(|(ptr, coeff)| (quotient_fq_to_u256(coeff), ptr)) + .collect(), + }, + used, + )); + } + } + None +} + +/// Find terms sharing one multiplicand with a complete seven-limb row. +fn try_quotient_bilin7_row_subshape( + terms: &[(Fq, &QuotientExpr)], +) -> Option<(QuotientLimbShape, Vec)> { + let mut pairs = Vec::<(usize, Fq, u16, u16)>::new(); + let mut ptrs = HashSet::new(); + for (idx, (coeff, expr)) in terms.iter().enumerate() { + if let Some((inner_coeff, lhs, rhs)) = quotient_product_mem_pair(expr) { + pairs.push((idx, *coeff * inner_coeff, lhs, rhs)); + ptrs.insert(lhs); + ptrs.insert(rhs); + } + } + + let mut candidates = ptrs.into_iter().collect::>(); + candidates.sort_unstable(); + for candidate in candidates { + let mut grouped = Vec::<(u16, Fq)>::new(); + let mut used = Vec::with_capacity(QUOTIENT_VM_LIMBS); + for (idx, coeff, lhs, rhs) in &pairs { + let other = if *lhs == candidate { + Some(*rhs) + } else if *rhs == candidate { + Some(*lhs) + } else { + None + }; + if let Some(other) = other { + add_grouped_limb_coeff(&mut grouped, other, *coeff); + used.push(*idx); + } + } + grouped.retain(|(_, coeff)| *coeff != Fq::ZERO); + if grouped.len() == QUOTIENT_VM_LIMBS && used.len() >= QUOTIENT_VM_LIMBS { + grouped.sort_by_key(|(ptr, _)| *ptr); + return Some(( + QuotientLimbShape::Bilin7Row { + lhs: candidate, + terms: grouped + .into_iter() + .map(|(ptr, coeff)| (quotient_fq_to_u256(coeff), ptr)) + .collect(), + }, + used, + )); + } + } + None +} + +/// Find a full 7x7 pairwise product grid inside a larger affine sum. +fn try_quotient_bilin7_pairwise_subshape( + terms: &[(Fq, &QuotientExpr)], +) -> Option<(QuotientLimbShape, Vec)> { + let mut pairs = Vec::<(usize, Fq, u16, u16)>::new(); + let mut ptrs = HashSet::new(); + for (idx, (coeff, expr)) in terms.iter().enumerate() { + if let Some((inner_coeff, lhs, rhs)) = quotient_product_mem_pair(expr) { + pairs.push((idx, *coeff * inner_coeff, lhs, rhs)); + ptrs.insert(lhs); + ptrs.insert(rhs); + } + } + + let bases = limb7_base_candidates(&ptrs); + for lhs_base in &bases { + for rhs_base in &bases { + let mut coeffs = vec![Fq::ZERO; QUOTIENT_VM_PAIRWISE_TERMS]; + let mut seen = [false; QUOTIENT_VM_PAIRWISE_TERMS]; + let mut used = Vec::with_capacity(QUOTIENT_VM_PAIRWISE_TERMS); + for (idx, coeff, lhs, rhs) in &pairs { + let direct = limb7_index(*lhs_base, *lhs).zip(limb7_index(*rhs_base, *rhs)); + let swapped = limb7_index(*lhs_base, *rhs).zip(limb7_index(*rhs_base, *lhs)); + let Some((i, j)) = direct.or(swapped) else { + continue; + }; + let term_idx = i * QUOTIENT_VM_LIMBS + j; + coeffs[term_idx] += *coeff; + seen[term_idx] = true; + used.push(*idx); + } + if seen.iter().any(|seen| !seen) { + continue; + } + + let mut by_sum = vec![None; QUOTIENT_VM_PAIRWISE_COEFFS]; + let mut ok = true; + for i in 0..QUOTIENT_VM_LIMBS { + for j in 0..QUOTIENT_VM_LIMBS { + let coeff = coeffs[i * QUOTIENT_VM_LIMBS + j]; + let slot = &mut by_sum[i + j]; + if let Some(expected) = slot { + if *expected != coeff { + ok = false; + break; + } + } else { + *slot = Some(coeff); + } + } + if !ok { + break; + } + } + if ok && used.len() >= QUOTIENT_VM_PAIRWISE_TERMS { + return Some(( + QuotientLimbShape::Bilin7Pairwise { + lhs_base: *lhs_base, + rhs_base: *rhs_base, + coeffs: by_sum + .into_iter() + .map(|coeff| { + quotient_fq_to_u256(coeff.expect("pairwise sum coefficient")) + }) + .collect(), + }, + used, + )); + } + } + } + None +} + +/// Return `(coefficient, ptr)` for a product with exactly one memory factor. +pub(crate) fn quotient_mem_term(expr: &QuotientExpr) -> Option<(Fq, u16)> { + let (coeff, ptrs) = quotient_product_mem_factors(expr)?; + if ptrs.len() == 1 { + Some((coeff, ptrs[0])) + } else { + None + } +} + +/// Return `(coefficient, lhs_ptr, rhs_ptr)` for a product with two memory +/// factors. +pub(crate) fn quotient_product_mem_pair(expr: &QuotientExpr) -> Option<(Fq, u16, u16)> { + let (coeff, ptrs) = quotient_product_mem_factors(expr)?; + if ptrs.len() == 2 { + Some((coeff, ptrs[0], ptrs[1])) + } else { + None + } +} + +/// Factor a product into its Fr coefficient and literal memory pointers. +/// +/// Token memory references are rejected because limb opcodes and fused product +/// forms store compact literal `u16` pointers. +pub(crate) fn quotient_product_mem_factors(expr: &QuotientExpr) -> Option<(Fq, Vec)> { + let mut leaves = Vec::new(); + if !collect_product_leaves(expr, &mut leaves) { + return None; + } + + let mut coeff = Fq::ONE; + let mut ptrs = Vec::new(); + for leaf in leaves { + match leaf { + QuotientLeaf::Const(value) => coeff *= quotient_fq_from_u256(value)?, + QuotientLeaf::Mem(QuotientMem::Literal(ptr)) => ptrs.push(u16::try_from(ptr).ok()?), + QuotientLeaf::Mem(QuotientMem::Token(_)) + | QuotientLeaf::Mem(QuotientMem::TokenOffset(_, _)) => return None, + } + } + Some((coeff, ptrs)) +} + +/// Add a coefficient into a grouped limb term. +pub(crate) fn add_grouped_limb_coeff(grouped: &mut Vec<(u16, Fq)>, ptr: u16, coeff: Fq) { + if let Some((_, existing)) = grouped.iter_mut().find(|(existing, _)| *existing == ptr) { + *existing += coeff; + } else { + grouped.push((ptr, coeff)); + } +} + +/// Find all base pointers that cover a contiguous seven-limb vector. +pub(crate) fn limb7_base_candidates(ptrs: &HashSet) -> Vec { + let mut bases = ptrs + .iter() + .copied() + .filter(|base| { + (0..QUOTIENT_VM_LIMBS).all(|idx| { + base.checked_add((idx * layout::WORD_BYTES) as u16) + .is_some_and(|ptr| ptrs.contains(&ptr)) + }) + }) + .collect::>(); + bases.sort_unstable(); + bases.dedup(); + bases +} + +/// Return the limb index of `ptr` relative to `base`, if it is in the vector. +pub(crate) fn limb7_index(base: u16, ptr: u16) -> Option { + let diff = ptr.checked_sub(base)?; + let word_bytes = layout::WORD_BYTES as u16; + if diff % word_bytes != 0 { + return None; + } + let idx = (diff / word_bytes) as usize; + (idx < QUOTIENT_VM_LIMBS).then_some(idx) +} + +/// Interpret a `U256` as a canonical BLS12-381 scalar field element. +/// +/// Values outside the field modulus are rejected; that is important when +/// folding parsed Yul literals back into Fr coefficients for shape recognition. +pub(crate) fn quotient_fq_from_u256(value: U256) -> Option { + let bytes = value.to_le_bytes::<32>(); + let repr = ::Repr::from(bytes); + Option::::from(Fq::from_repr(repr)) +} + +/// Encode a BLS12-381 scalar field element as a `U256`. +pub(crate) fn quotient_fq_to_u256(value: Fq) -> U256 { + fe_to_u256::(&value) +} + +/// Parse a generated Yul memory pointer expression into a VM memory reference. +pub(crate) fn parse_mem(ptr: &str) -> QuotientMem { + let ptr = ptr.trim(); + if let Some(value) = parse_u32_literal(ptr) { + QuotientMem::Literal(value) + } else if let Some(token) = mem_token(ptr) { + QuotientMem::Token(token) + } else if let Some(args) = call_args(ptr, "add") { + assert_eq!(args.len(), 2, "add pointer arity"); + let token = mem_token(args[0].trim()) + .unwrap_or_else(|| panic!("unsupported quotient mload base: {}", args[0])); + let offset = parse_u32_literal(args[1].trim()) + .unwrap_or_else(|| panic!("unsupported quotient mload offset: {}", args[1])); + QuotientMem::TokenOffset(token, offset) + } else { + panic!("unsupported quotient mload pointer: {ptr}"); + } +} + +/// Return whether a string is a decimal or hexadecimal integer literal. +pub(crate) fn is_literal(value: &str) -> bool { + let value = value.trim(); + value.starts_with("0x") || value.as_bytes().first().is_some_and(|byte| byte.is_ascii_digit()) +} + +/// Parse a decimal or hexadecimal `U256` literal. +pub(crate) fn parse_u256(value: &str) -> U256 { + let value = value.trim(); + if let Some(hex) = value.strip_prefix("0x") { + U256::from_str_radix(hex, 16) + .unwrap_or_else(|err| panic!("valid hex U256 `{value}`: {err:?}")) + } else { + U256::from_str_radix(value, 10) + .unwrap_or_else(|err| panic!("valid decimal U256 `{value}`: {err:?}")) + } +} + +/// Render a `U256` as the shortest stable hexadecimal Yul literal. +pub(crate) fn u256_string(value: U256) -> String { + if value.bit_len() < 64 { + format!("0x{:x}", value.as_limbs()[0]) + } else { + format!("0x{value:x}") + } +} + +/// Render BLS12-381 Fr `DELTA` for legacy/direct Yul paths. +pub(crate) fn fr_delta_literal() -> String { + u256_string(fe_to_u256::(&Fq::DELTA)) +} + +/// Parse a literal if it fits in `u32`. +pub(crate) fn parse_u32_literal(value: &str) -> Option { + if !is_literal(value) { + return None; + } + let parsed = parse_u256(value); + parsed.try_into().ok() +} + +/// Parse a literal if it fits in `usize`. +pub(crate) fn parse_usize_literal(value: &str) -> Option { + if !is_literal(value) { + return None; + } + let parsed = parse_u256(value); + parsed.try_into().ok() +} + +/// Resolve a generated Yul memory symbol to a VM token. +pub(crate) fn mem_token(name: &str) -> Option { + quotient_mem_token_from_name(name) +} + +/// Parsed form of the simple Yul assignments emitted by `Evaluator`. +/// +/// The parser is deliberately shallow. It is not a Yul parser; it accepts only +/// assignment syntax that this generator emits, which makes unsupported changes +/// fail loudly during code generation instead of silently changing verifier +/// semantics. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct YulAssignment { + /// Destination variable name. + pub(crate) dst: String, + /// Right-hand expression as source text. + pub(crate) expr: String, + /// Whether the assignment was introduced with `let`. + pub(crate) has_let: bool, +} + +/// Parse a generated Yul assignment line. +pub(crate) fn yul_assignment(line: &str) -> Option { + let line = line.trim(); + let (has_let, rest) = if let Some(rest) = line.strip_prefix("let") { + if !rest.starts_with(char::is_whitespace) { + return None; + } + (true, rest.trim_start()) + } else { + (false, line) + }; + let (dst, expr) = rest.split_once(":=")?; + let dst = dst.trim(); + let expr = expr.trim(); + if dst.is_empty() || expr.is_empty() { + return None; + } + Some(YulAssignment { + dst: dst.to_string(), + expr: expr.to_string(), + has_let, + }) +} + +/// Parse a generated `let dst := expr` assignment. +pub(crate) fn yul_let_assignment(line: &str) -> Option<(String, String)> { + let assignment = yul_assignment(line)?; + assignment.has_let.then_some((assignment.dst, assignment.expr)) +} + +/// Resolve a literal or previously bound constant variable to canonical text. +pub(crate) fn yul_const_value(value: &str, const_vars: &HashMap) -> Option { + let value = value.trim(); + if is_literal(value) { + Some(u256_string(parse_u256(value))) + } else { + const_vars.get(value).cloned() + } +} + +/// Parse `let dst := mulmod(lhs, rhs, r)`. +pub(crate) fn yul_mulmod_assignment(line: &str) -> Option<(String, String, String)> { + let (dst, expr) = yul_let_assignment(line)?; + let args = call_args(&expr, "mulmod")?; + if args.len() == 3 && args[2].trim() == "r" { + Some((dst, args[0].trim().to_string(), args[1].trim().to_string())) + } else { + None + } +} + +/// Parse `let dst := addmod(lhs, rhs, r)`. +pub(crate) fn yul_addmod_assignment(line: &str) -> Option<(String, String, String)> { + let (dst, expr) = yul_let_assignment(line)?; + let args = call_args(&expr, "addmod")?; + if args.len() == 3 && args[2].trim() == "r" { + Some((dst, args[0].trim().to_string(), args[1].trim().to_string())) + } else { + None + } +} + +/// Parse `mload(literal_ptr)`. +pub(crate) fn yul_mload_literal_expr(expr: &str) -> Option { + let args = call_args(expr.trim(), "mload")?; + if args.len() == 1 { + parse_usize_literal(args[0].trim()) + } else { + None + } +} + +/// Return top-level comma-separated call arguments for `name(...)`. +pub(crate) fn call_args(expr: &str, name: &str) -> Option> { + let expr = expr.trim(); + let rest = expr.strip_prefix(name)?.trim_start(); + let rest = rest.strip_prefix('(')?; + if !rest.ends_with(')') { + return None; + } + Some(split_top_level(&rest[..rest.len() - 1])) +} + +/// Split a comma-separated argument list while respecting nested calls. +pub(crate) fn split_top_level(input: &str) -> Vec { + let mut args = Vec::new(); + let mut depth = 0usize; + let mut start = 0usize; + for (idx, ch) in input.char_indices() { + match ch { + '(' => depth += 1, + ')' => depth = depth.checked_sub(1).expect("balanced quotient expression"), + ',' if depth == 0 => { + args.push(input[start..idx].trim().to_string()); + start = idx + ch.len_utf8(); + } + _ => {} + } + } + args.push(input[start..].trim().to_string()); + args +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn byte_compaction_groups_mixed_affine_sum_terms() { + let mut bytes = Vec::new(); + bytes.push(Q_OP_PUSH_CONST_U8); + bytes.push(0); + bytes.push(Q_OP_ADD_MUL_CONST_U8_MEM_U16); + bytes.extend_from_slice(&0x120u16.to_be_bytes()); + bytes.push(1); + bytes.push(Q_OP_ADD_MUL_MEM_MEM_CONST_U8); + bytes.extend_from_slice(&0x140u16.to_be_bytes()); + bytes.extend_from_slice(&0x160u16.to_be_bytes()); + bytes.push(2); + bytes.push(Q_OP_ADD_MUL_CONST_U8_MEM_U16); + bytes.extend_from_slice(&0x180u16.to_be_bytes()); + bytes.push(3); + bytes.push(Q_OP_ADD_MUL_MEM_MEM_CONST_U8); + bytes.extend_from_slice(&0x1a0u16.to_be_bytes()); + bytes.extend_from_slice(&0x1c0u16.to_be_bytes()); + bytes.push(4); + bytes.push(Q_OP_FOLD_MAIN); + + let compacted = compact_quotient_runs(&bytes); + assert_eq!(compacted[2], Q_OP_AFFINE_SUM); + assert_eq!(read_u16(&compacted, 3), 2); + assert_eq!(read_u16(&compacted, 5), 2); + assert_eq!(read_u16(&compacted, 7), 0x120); + assert_eq!(compacted[9], 1); + assert_eq!(read_u16(&compacted, 10), 0x180); + assert_eq!(compacted[12], 3); + assert_eq!(read_u16(&compacted, 13), 0x140); + assert_eq!(read_u16(&compacted, 15), 0x160); + assert_eq!(compacted[17], 2); + assert_eq!(read_u16(&compacted, 18), 0x1a0); + assert_eq!(read_u16(&compacted, 20), 0x1c0); + assert_eq!(compacted[22], 4); + + let max_stack = validate_quotient_program(&compacted) + .expect("mixed affine sum program should pass validation"); + assert_eq!(max_stack, 1); + } + + #[test] + fn byte_validator_rejects_empty_affine_sum_sides() { + for (lin_count, product_count) in [(0u16, 1u16), (1, 0)] { + let mut bytes = Vec::new(); + bytes.push(Q_OP_PUSH_CONST_U8); + bytes.push(0); + bytes.push(Q_OP_AFFINE_SUM); + bytes.extend_from_slice(&lin_count.to_be_bytes()); + bytes.extend_from_slice(&product_count.to_be_bytes()); + let err = validate_quotient_program(&bytes) + .expect_err("empty affine sum side should be rejected"); + assert!(err.contains("requires nonzero linear and product counts")); + } + } +} diff --git a/proofs/solidity-verifier/src/lowering/quotient_numerator/yul_emit.rs b/proofs/solidity-verifier/src/lowering/quotient_numerator/yul_emit.rs new file mode 100644 index 000000000..bc8242de0 --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/quotient_numerator/yul_emit.rs @@ -0,0 +1,1161 @@ +// SPDX-License-Identifier: CC0-1.0 +#![allow(clippy::useless_format)] + +//! Batched identity numerator emitter. +//! +//! This module walks the gates / permutation / lookup / trash arguments +//! stored in a `midnight_proofs::plonk::ConstraintSystem` and emits the +//! Yul lines that compute their per-row contributions to +//! `quotient_eval_numer` at the evaluation challenge `x`. +//! +//! The name is historical: this is not an evaluator for the quotient +//! polynomial `h(x)`. It reconstructs the batched identity numerator +//! `nu_y(x)`. The generated verifier later stores `-nu_y(x)` as the +//! expected opening scalar for the linearized commitment; the commitment +//! side already includes the quotient-limb factor `(1 - x^n)`. +//! +//! ## Step 4 status (2026-04-26) +//! +//! All four emitters are now ported: +//! +//! * [`Evaluator::gate_computations_tagged`] — per-gate `polynomials()` +//! * [`Evaluator::permutation_computations`] — boundary + product equality +//! * [`Evaluator::lookup_computations`] — boundary + helper + accumulator +//! * [`Evaluator::trashcan_computations`] — `compressed - (1-q)*trash` +//! +//! Each returns a `Vec<(Vec, String)>` where the inner pair is +//! `(yul_lines, final_var)`; the caller in `lowering/quotient.rs` folds the +//! final value into the compact quotient VM state or a native callback body. +//! +//! All emitters reference well-known Yul memory pointers that the +//! verifier prologue populates before invoking the quotient-numerator block. +//! Names follow the generated-verifier memory convention, including +//! `TRASH_CHALLENGE_MPTR` for the Midnight trash argument. + +use std::{cell::RefCell, cmp::Ordering, collections::HashMap}; + +use ff::{Field, PrimeField}; +use midnight_curves::Fq; +use midnight_proofs::plonk::{Any, Column, ConstraintSystem, Expression}; +use ruint::aliases::U256; + +use crate::lowering::encoding::{fe_to_u256, ConstraintSystemMeta, Data, Location, Value, Word}; + +#[derive(Debug)] +pub(crate) struct Evaluator<'a> { + /// Source constraint system whose identities are emitted. + cs: &'a ConstraintSystem, + /// Codegen metadata for query order and selector handling. + meta: &'a ConstraintSystemMeta, + /// Concrete memory/calldata handles for all queried values. + data: &'a Data, + /// Whether five repeated factors should be lowered through `q_pow5`. + use_pow5_helper: bool, + /// Local variable counter for the current emitted identity. + var_counter: RefCell, + /// Per-identity expression text to variable-name cache. + var_cache: RefCell>, +} + +/// Yul computation plus source metadata for one gate polynomial. +#[derive(Clone, Debug)] +pub(crate) struct GateComputation { + pub(crate) lines: Vec, + pub(crate) var: String, + pub(crate) simple_selector_index: Option, + pub(crate) gate_index: usize, + pub(crate) gate_name: String, + pub(crate) constraint_index: usize, + pub(crate) constraint_name: String, + pub(crate) polynomial_index: usize, +} + +impl<'a> Evaluator<'a> { + /// Create an evaluator bound to a constraint system, metadata, and data + /// map. + pub(crate) fn new( + cs: &'a ConstraintSystem, + meta: &'a ConstraintSystemMeta, + data: &'a Data, + ) -> Self { + Self { + cs, + meta, + data, + use_pow5_helper: false, + var_counter: Default::default(), + var_cache: Default::default(), + } + } + + /// Enable or disable the optional `q_pow5` Yul helper peephole. + pub(crate) fn with_pow5_helper(mut self, enabled: bool) -> Self { + self.use_pow5_helper = enabled; + self + } + + /// Clear local variable and expression caches. + pub(crate) fn reset_locals(&self) { + self.reset(); + } + + /// Emit Yul for a single Halo2 expression without changing caller state. + pub(crate) fn evaluate_expression(&self, expression: &Expression) -> (Vec, String) { + self.evaluate(expression) + } + + /// Compress expressions with a caller-provided challenge variable. + /// + /// This is used by structured lookup/trash paths that hoist theta or the + /// trash challenge once and then reuse the variable across loop bodies. + pub(crate) fn compress_expressions_with_challenge_var( + &self, + expressions: &[Expression], + challenge_var: &str, + ) -> (Vec, String) { + self.compress_expressions(expressions, challenge_var) + } + + /// Emit a shared-prefix optimization for parallel lookup inputs. + /// + /// When every parallel input has the same prefix and its final limb is laid + /// out in adjacent memory words, the generated Yul evaluates the prefix + /// once and loops over the tails. This mirrors LogUp's θ-compression while + /// avoiding repeated arithmetic in wide range-check lookups. + pub(crate) fn lookup_shared_prefix_f_plus_beta( + &self, + input_chunk: &[Vec>], + challenge_var: &str, + beta_var: &str, + f_plus_beta_mptr: &str, + ) -> Option> { + if input_chunk.len() < 3 { + return None; + } + + let first = input_chunk.first()?; + if first.is_empty() { + return None; + } + let prefix_len = first.len().checked_sub(1)?; + if prefix_len == 0 { + return None; + } + + if input_chunk + .iter() + .any(|exprs| exprs.len() != first.len() || exprs[..prefix_len] != first[..prefix_len]) + { + return None; + } + + let base_ptr = self.expression_memory_ptr(first.last()?)?; + for (idx, exprs) in input_chunk.iter().enumerate() { + if self.expression_memory_ptr(exprs.last()?)? != base_ptr + idx * 0x20 { + return None; + } + } + + let mut lines = Vec::new(); + let (mut prefix_lines, prefix_var) = + self.compress_expressions(&first[..prefix_len], challenge_var); + lines.append(&mut prefix_lines); + let prefix_scaled = self.fresh_var(); + lines.push(format!( + "let {prefix_scaled} := mulmod({prefix_var}, {challenge_var}, r)" + )); + lines.push(format!( + "for {{ let q_lookup_shared_i := 0 }} lt(q_lookup_shared_i, {}) {{ q_lookup_shared_i := add(q_lookup_shared_i, 1) }} {{", + input_chunk.len() + )); + lines.push("let q_lookup_shared_off := shl(5, q_lookup_shared_i)".to_string()); + lines.push(format!( + "let q_lookup_shared_tail := mload(add({base_ptr:#x}, q_lookup_shared_off))" + )); + lines.push(format!( + "let q_lookup_shared_compressed := addmod({prefix_scaled}, q_lookup_shared_tail, r)" + )); + lines.push(format!( + "mstore(add({f_plus_beta_mptr}, q_lookup_shared_off), addmod(q_lookup_shared_compressed, {beta_var}, r))" + )); + lines.push("}".to_string()); + Some(lines) + } + + // ---------------------------------------------------------------- + // Gate emitter: one constraint per gate-polynomial. + // ---------------------------------------------------------------- + + /// Emit each gate polynomial and tag it with its simple-selector + /// fixed-column index (if any). + /// Mirrors `partially_evaluate_identities` in + /// `midfall/proofs/src/plonk/mod.rs`: for each gate, the simple + /// selector index is `gate.queried_selectors().filter(|s| + /// s.is_simple()).next().map(|s| s.index())`. After + /// `directly_convert_selectors_to_fixed`, a simple selector's + /// `index()` equals the fixed column index of its replacement + /// (selector indices were shifted by `nr_fixed_columns`). + pub(crate) fn gate_computations_tagged(&self) -> Vec { + self.cs + .gates() + .iter() + .enumerate() + .flat_map(|(gate_index, gate)| { + let simple_idx = + gate.queried_selectors().iter().find(|s| s.is_simple()).map(|s| s.index()); + gate.polynomials() + .iter() + .enumerate() + .map(move |(polynomial_index, poly)| { + let (lines, var) = self.evaluate_and_reset(poly); + GateComputation { + lines, + var, + simple_selector_index: simple_idx, + gate_index, + gate_name: gate.name().to_string(), + constraint_index: polynomial_index, + constraint_name: gate.constraint_name(polynomial_index).to_string(), + polynomial_index, + } + }) + .collect::>() + }) + .collect() + } + + // ---------------------------------------------------------------- + // Permutation emitter. + // + // Mirrors `midfall/proofs/src/plonk/permutation.rs::expressions`: + // + // 1. l_0 * (1 - z_0) [first set] + // 2. l_last * (z_n^2 - z_n) [last set] + // 3. l_0 * (z_i - z_{i-1}_last) [for i >= 1] + // 4. (1 - (l_last + l_blind)) * (left_i - right_i) [for each set] where + // left_i = z_i_next * âˆ_{c in chunk_i} (eval(c) + β·s(c) + γ) right_i = + // z_i * âˆ_{c in chunk_i} (eval(c) + δ_pow + γ) δ_pow starts at + // β·x·δ^(i*chunk_len) and is *= δ each step + // + // δ is the field's `PrimeField::DELTA` constant (a generator of + // a small-order multiplicative subgroup that separates the chunk + // permutation cosets). + // ---------------------------------------------------------------- + + /// Emit permutation numerator identities. + /// + /// Upstream reference: `plonk/evaluation.rs` describes the same four + /// constraints over `l_0`, `l_last`, `l_blind`, and the permutation product + /// chunks. The generator emits them one identity at a time so the quotient + /// y-batch order matches `partially_evaluate_identities`. + pub(crate) fn permutation_computations(&self) -> Vec<(Vec, String)> { + if self.meta.num_permutation_zs == 0 { + return Vec::new(); + } + + let chunk_len = self.meta.permutation_chunk_len; + let columns = &self.meta.permutation_columns; + let z_evals = &self.data.permutation_z_evals; + + let mut out: Vec<(Vec, String)> = Vec::new(); + + // 1. First-set boundary: l_0 * (1 - z_0_cur) + { + self.reset(); + let mut lines = Vec::new(); + let z0 = z_evals.first().expect("perm sets non-empty").0.to_string(); + let l0 = self.fresh_var(); + lines.push(format!("let {l0} := mload(L_0_MPTR)")); + let one_minus_z0 = self.fresh_var(); + lines.push(format!("let {one_minus_z0} := addmod(1, sub(r, {z0}), r)")); + let bnd_first = self.fresh_var(); + lines.push(format!( + "let {bnd_first} := mulmod({l0}, {one_minus_z0}, r)" + )); + out.push((lines, bnd_first)); + } + + // 2. Last-set boundary: l_last * (z_n^2 - z_n) + { + self.reset(); + let mut lines = Vec::new(); + let zn = z_evals.last().expect("perm sets non-empty").0.to_string(); + let llast = self.fresh_var(); + lines.push(format!("let {llast} := mload(L_LAST_MPTR)")); + let zn_sq = self.fresh_var(); + lines.push(format!("let {zn_sq} := mulmod({zn}, {zn}, r)")); + let zn_sq_minus_zn = self.fresh_var(); + lines.push(format!( + "let {zn_sq_minus_zn} := addmod({zn_sq}, sub(r, {zn}), r)" + )); + let bnd_last = self.fresh_var(); + lines.push(format!( + "let {bnd_last} := mulmod({llast}, {zn_sq_minus_zn}, r)" + )); + out.push((lines, bnd_last)); + } + + // 3. Set-to-set continuity: l_0 * (z_i_cur - z_{i-1}_last) + for i in 1..self.meta.num_permutation_zs { + self.reset(); + let mut lines = Vec::new(); + let zi = z_evals[i].0.to_string(); + let z_prev_last = + z_evals[i - 1].2.as_ref().expect("non-last set has last eval").to_string(); + let l0 = self.fresh_var(); + lines.push(format!("let {l0} := mload(L_0_MPTR)")); + let diff = self.fresh_var(); + lines.push(format!( + "let {diff} := addmod({zi}, sub(r, {z_prev_last}), r)" + )); + let cont = self.fresh_var(); + lines.push(format!("let {cont} := mulmod({l0}, {diff}, r)")); + out.push((lines, cont)); + } + + // 4. Per-set product equality. + let delta = fe_to_u256::(&Fq::DELTA); + for (set_idx, ((z_cur, z_next, _), chunk_cols)) in + z_evals.iter().zip(columns.chunks(chunk_len)).enumerate() + { + self.reset(); + let mut lines = Vec::new(); + + // active_rows = 1 - (l_last + l_blind) + let l_last = self.fresh_var(); + lines.push(format!("let {l_last} := mload(L_LAST_MPTR)")); + let l_blind = self.fresh_var(); + lines.push(format!("let {l_blind} := mload(L_BLIND_MPTR)")); + let l_last_plus_blind = self.fresh_var(); + lines.push(format!( + "let {l_last_plus_blind} := addmod({l_last}, {l_blind}, r)" + )); + let active = self.fresh_var(); + lines.push(format!( + "let {active} := addmod(1, sub(r, {l_last_plus_blind}), r)" + )); + + let beta = self.fresh_var(); + lines.push(format!("let {beta} := mload(BETA_MPTR)")); + let gamma = self.fresh_var(); + lines.push(format!("let {gamma} := mload(GAMMA_MPTR)")); + + // left = z_next * ∠(eval(c) + β * s_eval(c) + γ) + let left = self.fresh_var(); + lines.push(format!("let {left} := {z_next}")); + for col in chunk_cols { + let col_eval = self.eval_at(col, 0); + let s_eval = self + .data + .permutation_evals + .get(col) + .expect("permutation eval present") + .to_string(); + let beta_s = self.fresh_var(); + lines.push(format!("let {beta_s} := mulmod({beta}, {s_eval}, r)")); + let term = self.fresh_var(); + lines.push(format!( + "let {term} := addmod(addmod({col_eval}, {beta_s}, r), {gamma}, r)" + )); + lines.push(format!("{left} := mulmod({left}, {term}, r)")); + } + + // right = z_cur * ∠(eval(c) + δ_pow + γ) + // δ_pow_0 = β * x * δ^(set_idx * chunk_len) + // δ_pow_{j+1} = δ_pow_j * δ + let initial_delta_power = Fq::DELTA.pow_vartime([(set_idx * chunk_len) as u64]); + let initial_delta_u256 = fe_to_u256::(&initial_delta_power); + let delta_pow = self.fresh_var(); + lines.push(format!( + "let {delta_pow} := mulmod(mulmod({beta}, mload(X_MPTR), r), {}, r)", + u256_string(initial_delta_u256) + )); + let right = self.fresh_var(); + lines.push(format!("let {right} := {z_cur}")); + let last_col_idx = chunk_cols.len().saturating_sub(1); + for (col_pos, col) in chunk_cols.iter().enumerate() { + let col_eval = self.eval_at(col, 0); + let term = self.fresh_var(); + lines.push(format!( + "let {term} := addmod(addmod({col_eval}, {delta_pow}, r), {gamma}, r)" + )); + lines.push(format!("{right} := mulmod({right}, {term}, r)")); + if col_pos != last_col_idx { + lines.push(format!( + "{delta_pow} := mulmod({delta_pow}, {}, r)", + u256_string(delta) + )); + } + } + + let diff = self.fresh_var(); + lines.push(format!("let {diff} := addmod({left}, sub(r, {right}), r)")); + let constraint = self.fresh_var(); + lines.push(format!("let {constraint} := mulmod({active}, {diff}, r)")); + out.push((lines, constraint)); + } + + out + } + + // ---------------------------------------------------------------- + // LogUp emitter. + // + // Mirrors `midfall/proofs/src/plonk/logup.rs::Evaluated::expressions`: + // + // For each lookup: + // boundary = (l_0 + l_last) * z_eval + // for each chunk c: + // compressed_inputs[j] = θ-fold-compress(input_chunk_c[j]) + // f_plus_β[j] = compressed_inputs[j] + β + // P = âˆ_j f_plus_β[j] + // partial[j] = P / f_plus_β[j] (computed via prefix*suffix) + // sum = Σ_j partial[j] + // helper_constraint = h_eval[c] * P - sum + // accumulator_constraint = + // (z_next - z - selector·Σh_eval) * (compressed_table + β) + m_eval + // all multiplied by active_rows = 1 - (l_last + l_blind) + // ---------------------------------------------------------------- + + /// Emit LogUp lookup numerator identities. + /// + /// The helper identity checks `h * prod(f_j + beta) = sum prod_{k!=j}` and + /// the accumulator identity checks + /// `(Z_next - Z - selector * sum(h)) * (table + beta) + m = 0`, with the + /// active-row gate applied as in the Rust verifier. + pub(crate) fn lookup_computations(&self) -> Vec<(Vec, String)> { + if self.meta.num_lookups == 0 { + return Vec::new(); + } + + let cs_degree = self.cs.degree(); + let mut out: Vec<(Vec, String)> = Vec::new(); + + for (lookup_idx, lookup) in self.cs.lookups().iter().enumerate() { + let chunked = lookup.chunk_by_degree(cs_degree); + let (m_eval, h_evals, z_eval, z_next_eval) = &self.data.lookup_evals[lookup_idx]; + + // Boundary: (l_0 + l_last) * z_eval + { + self.reset(); + let mut lines = Vec::new(); + let l_0 = self.fresh_var(); + lines.push(format!("let {l_0} := mload(L_0_MPTR)")); + let l_last = self.fresh_var(); + lines.push(format!("let {l_last} := mload(L_LAST_MPTR)")); + let l_sum = self.fresh_var(); + lines.push(format!("let {l_sum} := addmod({l_0}, {l_last}, r)")); + let bnd = self.fresh_var(); + lines.push(format!("let {bnd} := mulmod({l_sum}, {z_eval}, r)")); + out.push((lines, bnd)); + } + + // Cached selector expression for the accumulator constraint + // below. Re-evaluated there because each constraint resets + // the per-emitter var cache; vars cannot leak between + // `(Vec, String)` entries. + let selector_expr = chunked.selector_expression(); + + for (input_chunk, h_eval) in + chunked.input_expression_chunks().iter().zip(h_evals.iter()) + { + self.reset(); + let mut lines = Vec::new(); + + let beta = self.fresh_var(); + lines.push(format!("let {beta} := mload(BETA_MPTR)")); + + // Compute compressed_input + β for each parallel-lookup + // entry in this chunk. + let mut f_plus_beta_vars: Vec = Vec::new(); + for parallel_input in input_chunk.iter() { + let compressed = self.compress_expressions_with_theta(parallel_input); + let (mut compressed_lines, compressed_var) = compressed; + lines.append(&mut compressed_lines); + let fb = self.fresh_var(); + lines.push(format!("let {fb} := addmod({compressed_var}, {beta}, r)")); + f_plus_beta_vars.push(fb); + } + + let k = f_plus_beta_vars.len(); + if k == 0 { + // Empty chunk shouldn't happen but emit a no-op. + let zero = self.fresh_var(); + lines.push(format!("let {zero} := 0")); + out.push((lines, zero)); + continue; + } + + // P = âˆ_j f_plus_β[j] + let p = self.fresh_var(); + lines.push(format!("let {p} := {}", f_plus_beta_vars[0])); + for fb in &f_plus_beta_vars[1..] { + lines.push(format!("{p} := mulmod({p}, {fb}, r)")); + } + + // Build partial products via prefix * suffix. + // prefix[j] = âˆ_{ij} f_plus_β[i], suffix[k-1] = 1 + // partial[j] = prefix[j] * suffix[j] + let mut prefix_vars: Vec = Vec::with_capacity(k); + { + let p0 = self.fresh_var(); + lines.push(format!("let {p0} := 1")); + prefix_vars.push(p0); + for j in 1..k { + let pj = self.fresh_var(); + lines.push(format!( + "let {pj} := mulmod({}, {}, r)", + prefix_vars[j - 1], + f_plus_beta_vars[j - 1] + )); + prefix_vars.push(pj); + } + } + let mut suffix_vars: Vec = vec![String::new(); k]; + { + let sk = self.fresh_var(); + lines.push(format!("let {sk} := 1")); + suffix_vars[k - 1] = sk; + for j in (0..k - 1).rev() { + let sj = self.fresh_var(); + lines.push(format!( + "let {sj} := mulmod({}, {}, r)", + suffix_vars[j + 1], + f_plus_beta_vars[j + 1] + )); + suffix_vars[j] = sj; + } + } + + // sum = Σ_j prefix[j] * suffix[j] + let sum_var = self.fresh_var(); + lines.push(format!( + "let {sum_var} := mulmod({}, {}, r)", + prefix_vars[0], suffix_vars[0] + )); + for j in 1..k { + let term = self.fresh_var(); + lines.push(format!( + "let {term} := mulmod({}, {}, r)", + prefix_vars[j], suffix_vars[j] + )); + lines.push(format!("{sum_var} := addmod({sum_var}, {term}, r)")); + } + + // helper_constraint = h_eval * P - sum + let h_p = self.fresh_var(); + lines.push(format!("let {h_p} := mulmod({h_eval}, {p}, r)")); + let helper_c = self.fresh_var(); + lines.push(format!( + "let {helper_c} := addmod({h_p}, sub(r, {sum_var}), r)" + )); + out.push((lines, helper_c)); + } + + // Accumulator constraint: + // active * ((z_next - z - s · Σh) · (t + β) + m) + { + self.reset(); + let mut lines = Vec::new(); + + let l_last = self.fresh_var(); + lines.push(format!("let {l_last} := mload(L_LAST_MPTR)")); + let l_blind = self.fresh_var(); + lines.push(format!("let {l_blind} := mload(L_BLIND_MPTR)")); + let active = self.fresh_var(); + lines.push(format!( + "let {active} := addmod(1, sub(r, addmod({l_last}, {l_blind}, r)), r)" + )); + + let beta = self.fresh_var(); + lines.push(format!("let {beta} := mload(BETA_MPTR)")); + + // Σ_h h_eval[c] + let sum_h = self.fresh_var(); + lines.push(format!("let {sum_h} := {}", h_evals[0])); + for h in &h_evals[1..] { + lines.push(format!("{sum_h} := addmod({sum_h}, {h}, r)")); + } + + // selector eval (full Expression; not necessarily a + // single column query). + let (mut sel_lines, sel_var) = self.evaluate(selector_expr); + lines.append(&mut sel_lines); + + // s · Σh + let s_sum_h = self.fresh_var(); + lines.push(format!("let {s_sum_h} := mulmod({sel_var}, {sum_h}, r)")); + + // diff = z_next - z - s·Σh + let diff = self.fresh_var(); + lines.push(format!( + "let {diff} := addmod({}, sub(r, addmod({}, {s_sum_h}, r)), r)", + z_next_eval, z_eval + )); + + // compressed_table = θ-fold-compress(table_expressions) + let (mut t_lines, t_var) = + self.compress_expressions_with_theta(chunked.table_expressions()); + lines.append(&mut t_lines); + let t_plus_beta = self.fresh_var(); + lines.push(format!("let {t_plus_beta} := addmod({t_var}, {beta}, r)")); + + // (diff) · (t + β) + m_eval + let core = self.fresh_var(); + lines.push(format!( + "let {core} := addmod(mulmod({diff}, {t_plus_beta}, r), {}, r)", + m_eval + )); + + let acc_c = self.fresh_var(); + lines.push(format!("let {acc_c} := mulmod({active}, {core}, r)")); + + out.push((lines, acc_c)); + } + } + + out + } + + // ---------------------------------------------------------------- + // Trashcan emitter. + // + // Mirrors `midfall/proofs/src/plonk/trash.rs::Evaluated::expressions`: + // + // For each trashcan: + // compressed = θ_trash-fold(constraint_expressions) + // selector = eval(argument.selector) + // constraint = compressed - (1 - selector) * trash_eval + // + // Note: midnight-proofs uses the *trash_challenge* (a separate + // squeeze) as the compression Ï„, NOT θ. We expose it as + // TRASH_CHALLENGE_MPTR; the Yul template (Step 6) is responsible for + // squeezing and storing it before the quotient eval block. + // ---------------------------------------------------------------- + + /// Emit trash argument numerator identities. + /// + /// Trash uses a dedicated Fiat-Shamir challenge, not theta, to compress its + /// constraint expressions before subtracting the inactive-row trash value. + pub(crate) fn trashcan_computations(&self) -> Vec<(Vec, String)> { + if self.meta.num_trashcans == 0 { + return Vec::new(); + } + + let mut out: Vec<(Vec, String)> = Vec::new(); + + for (idx, argument) in self.cs.trashcans().iter().enumerate() { + self.reset(); + let mut lines = Vec::new(); + + // compressed = fold((acc, e) -> acc * Ï„ + e) over + // argument.constraint_expressions(). Note this is the same + // recipe as the logup θ-compression, but with the trash + // challenge as the variable. The challenge is loaded lazily + // so an empty constraint list does not emit an unused let. + let constraint_exprs = argument.constraint_expressions(); + let trash_challenge = if constraint_exprs.is_empty() { + None + } else { + let var = self.fresh_var(); + lines.push(format!("let {var} := mload(TRASH_CHALLENGE_MPTR)")); + Some(var) + }; + + let mut compressed_var: Option = None; + for expr in constraint_exprs { + let (mut e_lines, e_var) = self.evaluate(expr); + lines.append(&mut e_lines); + let next = self.fresh_var(); + let prev = compressed_var.unwrap_or_else(|| "0".to_string()); + let tau = trash_challenge + .as_deref() + .expect("trash_challenge present when expressions non-empty"); + lines.push(format!( + "let {next} := addmod(mulmod({prev}, {tau}, r), {e_var}, r)" + )); + compressed_var = Some(next); + } + let compressed = compressed_var.unwrap_or_else(|| { + let zero = self.fresh_var(); + lines.push(format!("let {zero} := 0")); + zero + }); + + // selector eval + let (mut sel_lines, sel_var) = self.evaluate(argument.selector()); + lines.append(&mut sel_lines); + + // (1 - q) * trash_eval + let one_minus_q = self.fresh_var(); + lines.push(format!( + "let {one_minus_q} := addmod(1, sub(r, {sel_var}), r)" + )); + let trash_eval = self.data.trashcan_evals[idx].to_string(); + let scaled = self.fresh_var(); + lines.push(format!( + "let {scaled} := mulmod({one_minus_q}, {trash_eval}, r)" + )); + + // constraint = compressed - (1 - q) * trash_eval + let c = self.fresh_var(); + lines.push(format!( + "let {c} := addmod({compressed}, sub(r, {scaled}), r)" + )); + out.push((lines, c)); + } + + out + } + + // ---------------------------------------------------------------- + // Helpers + // ---------------------------------------------------------------- + + /// θ-compress a slice of expressions: + /// compressed = fold((acc, e) -> acc * θ + e) over expressions + /// + /// Returns the Yul lines + the final variable name. Uses a fresh + /// `theta` mload at the start; relies on the caller's reset cycle + /// to deduplicate within a constraint. + fn compress_expressions_with_theta( + &self, + expressions: &[Expression], + ) -> (Vec, String) { + if expressions.is_empty() { + return self.init_var("0x0", None); + } + + let mut lines = Vec::new(); + let theta = self.fresh_var(); + lines.push(format!("let {theta} := mload(THETA_MPTR)")); + + let (mut compressed_lines, final_var) = self.compress_expressions(expressions, &theta); + lines.append(&mut compressed_lines); + (lines, final_var) + } + + /// Fold expressions as `acc = acc * challenge + expr`. + fn compress_expressions( + &self, + expressions: &[Expression], + challenge_var: &str, + ) -> (Vec, String) { + let mut lines = Vec::new(); + let mut acc_var: Option = None; + for expr in expressions { + let (mut e_lines, e_var) = self.evaluate(expr); + lines.append(&mut e_lines); + let next = self.fresh_var(); + let prev = acc_var.unwrap_or_else(|| "0".to_string()); + lines.push(format!( + "let {next} := addmod(mulmod({prev}, {challenge_var}, r), {e_var}, r)" + )); + acc_var = Some(next); + } + let final_var = acc_var.unwrap_or_else(|| { + let zero = self.fresh_var(); + lines.push(format!("let {zero} := 0")); + zero + }); + (lines, final_var) + } + + /// Return the concrete memory pointer for a simple query expression. + /// + /// This is used only by loop optimizers that require adjacent memory-backed + /// evals; non-query expressions or symbolic pointers return `None`. + fn expression_memory_ptr(&self, expression: &Expression) -> Option { + let word = match expression { + Expression::Advice(query) => self + .data + .advice_evals + .get(&(query.column_index(), query.rotation().0)) + .copied()?, + Expression::Fixed(query) => { + let column_index = query.column_index(); + if self.meta.simple_selector_cols.contains(&column_index) { + return None; + } + self.data.fixed_evals.get(&(column_index, query.rotation().0)).copied()? + } + Expression::Instance(query) => { + let column_index = query.column_index(); + if column_index < self.meta.num_committed_instances { + self.data + .committed_instance_evals + .get(&(column_index, query.rotation().0)) + .copied()? + } else { + self.data.instance_eval + } + } + _ => return None, + }; + + if word.loc() != Location::Memory { + return None; + } + + match word.ptr().value() { + Value::Integer(offset) if offset >= 0 => Some(offset as usize), + _ => None, + } + } + + /// Recognize `a * a * a * a * a` when the Yul pow5 helper is enabled. + fn pow5_base_expr<'b>(&self, expression: &'b Expression) -> Option<&'b Expression> { + if !self.use_pow5_helper { + return None; + } + + let mut factors = Vec::new(); + collect_product_factors(expression, &mut factors); + if factors.len() != 5 { + return None; + } + + let base = factors[0]; + if factors.iter().skip(1).all(|factor| *factor == base) { + Some(base) + } else { + None + } + } + + /// Resolve a `Column` evaluation at a rotation. Used by the + /// permutation emitter (which works in `Column` form rather + /// than `Expression`). + pub(crate) fn eval_at(&self, column: &Column, rotation: i32) -> String { + let col_idx = column.index(); + match column.column_type() { + Any::Advice(_) => self + .data + .advice_evals + .get(&(col_idx, rotation)) + .expect("advice eval present in permutation chunk") + .to_string(), + Any::Fixed => { + if self.meta.simple_selector_cols.contains(&col_idx) { + "0x1".to_string() + } else { + self.data + .fixed_evals + .get(&(col_idx, rotation)) + .expect("fixed eval present in permutation chunk") + .to_string() + } + } + Any::Instance => self.instance_eval_at(col_idx, rotation), + } + } + + /// Allocate the next local Yul variable name. + fn fresh_var(&self) -> String { + self.next_var() + } + + /// Reset local-variable numbering and expression cache. + fn reset(&self) { + *self.var_counter.borrow_mut() = Default::default(); + *self.var_cache.borrow_mut() = Default::default(); + } + + /// Emit one expression from a clean local state. + fn evaluate_and_reset(&self, expression: &Expression) -> (Vec, String) { + self.reset(); + self.evaluate(expression) + } + + /// Emit an expression, first trying additive-term fusion. + fn evaluate(&self, expression: &Expression) -> (Vec, String) { + if let Some(result) = self.evaluate_sum_with_coeff_and_const(expression) { + return result; + } + + self.evaluate_basic(expression) + } + + /// Try to flatten a sum into constants and scaled terms before emission. + /// + /// This avoids nested `addmod` trees and lets product/constant terms use + /// fewer temporary variables in generated Yul. + fn evaluate_sum_with_coeff_and_const( + &self, + expression: &Expression, + ) -> Option<(Vec, String)> { + let mut terms = Vec::new(); + let mut constant = Fq::ZERO; + Self::collect_sum_terms(expression, Fq::ONE, &mut constant, &mut terms); + + terms.retain(|(coeff, _)| !coeff.is_zero_vartime()); + let has_constant = !constant.is_zero_vartime(); + let should_fuse = has_constant + || terms.len() > 1 + || terms.first().is_some_and(|(coeff, _)| *coeff != Fq::ONE); + if !should_fuse { + return None; + } + + let mut lines = Vec::new(); + let mut acc = if has_constant { + let (const_lines, const_var) = + self.init_var(u256_string(fe_to_u256::(&constant)), None); + lines.extend(const_lines); + Some(const_var) + } else { + None + }; + + for (coeff, term) in terms { + let (mut term_lines, term_var) = self.evaluate_scaled_term(term, coeff); + lines.append(&mut term_lines); + acc = Some(match acc { + Some(acc_var) => { + let (add_lines, add_var) = + self.init_var(format!("addmod({acc_var}, {term_var}, r)"), None); + lines.extend(add_lines); + add_var + } + None => term_var, + }); + } + + Some(match acc { + Some(var) => (lines, var), + None => self.init_var("0x0", None), + }) + } + + /// Recursively collect additive terms with their accumulated coefficient. + fn collect_sum_terms<'b>( + expression: &'b Expression, + coeff: Fq, + constant: &mut Fq, + terms: &mut Vec<(Fq, &'b Expression)>, + ) { + match expression { + Expression::Constant(value) => { + *constant += coeff * value; + } + Expression::Negated(inner) => { + Self::collect_sum_terms(inner, -coeff, constant, terms); + } + Expression::Sum(lhs, rhs) => { + Self::collect_sum_terms(lhs, coeff, constant, terms); + Self::collect_sum_terms(rhs, coeff, constant, terms); + } + Expression::Scaled(inner, scale) => { + Self::collect_sum_terms(inner, coeff * scale, constant, terms); + } + _ => terms.push((coeff, expression)), + } + } + + /// Emit one term multiplied by a known Fr coefficient. + fn evaluate_scaled_term( + &self, + expression: &Expression, + coeff: Fq, + ) -> (Vec, String) { + let coeff_is_one = coeff == Fq::ONE; + + if let Some(base) = self.pow5_base_expr(expression) { + let (mut lines, base_var) = self.evaluate_basic(base); + let (pow_lines, pow_var) = self.init_var(format!("q_pow5({base_var})"), None); + lines.extend(pow_lines); + if coeff_is_one { + return (lines, pow_var); + } + + let (coeff_lines, coeff_var) = + self.init_var(u256_string(fe_to_u256::(&coeff)), None); + lines.extend(coeff_lines); + let (scale_lines, out_var) = + self.init_var(format!("mulmod({pow_var}, {coeff_var}, r)"), None); + lines.extend(scale_lines); + return (lines, out_var); + } + + if let Expression::Product(lhs, rhs) = expression { + let (mut lines, lhs_var) = self.evaluate_basic(lhs); + let (mut rhs_lines, rhs_var) = self.evaluate_basic(rhs); + lines.append(&mut rhs_lines); + let expr = if coeff_is_one { + format!("mulmod({lhs_var}, {rhs_var}, r)") + } else { + let (coeff_lines, coeff_var) = + self.init_var(u256_string(fe_to_u256::(&coeff)), None); + lines.extend(coeff_lines); + format!("mulmod(mulmod({lhs_var}, {rhs_var}, r), {coeff_var}, r)") + }; + let (out_lines, out_var) = self.init_var(expr, None); + lines.extend(out_lines); + return (lines, out_var); + } + + let (mut lines, var) = self.evaluate_basic(expression); + if coeff_is_one { + return (lines, var); + } + + let (coeff_lines, coeff_var) = self.init_var(u256_string(fe_to_u256::(&coeff)), None); + lines.extend(coeff_lines); + let (scale_lines, out_var) = self.init_var(format!("mulmod({var}, {coeff_var}, r)"), None); + lines.extend(scale_lines); + (lines, out_var) + } + + /// Emit an expression through the native `Expression::evaluate` visitor. + fn evaluate_basic(&self, expression: &Expression) -> (Vec, String) { + if let Some(base) = self.pow5_base_expr(expression) { + let (mut lines, base_var) = self.evaluate_basic(base); + let (pow_lines, pow_var) = self.init_var(format!("q_pow5({base_var})"), None); + lines.extend(pow_lines); + return (lines, pow_var); + } + + // midnight-proofs `Expression` carries the full frontend + // variants: Constant / Selector / Fixed / Advice / Instance / + // Challenge / Negated / Sum / Product / Scaled. We do not expect + // to see `Selector` here because virtual selectors are removed + // during `directly_convert_selectors_to_fixed`. + expression.evaluate( + &|scalar| self.init_var(u256_string(fe_to_u256::(&scalar)), None), + &|_| panic!("virtual selectors must be removed before codegen"), + &|query| { + let column_index = query.column_index(); + let rotation = query.rotation().0; + if self.meta.simple_selector_cols.contains(&column_index) { + // Simple selectors have no eval slot; the verifier + // semantics insert F::ONE. + self.init_var("0x1".to_string(), None) + } else { + let eval: Word = *self + .data + .fixed_evals + .get(&(column_index, rotation)) + .expect("fixed eval present"); + let var_name = column_eval_var("f", column_index, rotation); + self.init_var(eval.to_string(), Some(var_name)) + } + }, + &|query| { + let column_index = query.column_index(); + let rotation = query.rotation().0; + let eval: Word = *self + .data + .advice_evals + .get(&(column_index, rotation)) + .expect("advice eval present"); + let var_name = column_eval_var("a", column_index, rotation); + self.init_var(eval.to_string(), Some(var_name)) + }, + &|query| { + let column_index = query.column_index(); + let rotation = query.rotation().0; + let eval = self.instance_eval_at(column_index, rotation); + self.init_var(eval, Some(column_eval_var("i", column_index, rotation))) + }, + &|challenge| { + self.init_var( + self.data.challenges[challenge.index()], + Some(format!("c_{}", challenge.index())), + ) + }, + &|(mut acc, var)| { + let (lines, var) = self.init_var(format!("addmod(0, sub(r, {var}), r)"), None); + acc.extend(lines); + (acc, var) + }, + &|(mut lhs_acc, lhs_var), (rhs_acc, rhs_var)| { + let (lines, var) = self.init_var(format!("addmod({lhs_var}, {rhs_var}, r)"), None); + lhs_acc.extend(rhs_acc); + lhs_acc.extend(lines); + (lhs_acc, var) + }, + &|(mut lhs_acc, lhs_var), (rhs_acc, rhs_var)| { + let (lines, var) = self.init_var(format!("mulmod({lhs_var}, {rhs_var}, r)"), None); + lhs_acc.extend(rhs_acc); + lhs_acc.extend(lines); + (lhs_acc, var) + }, + &|(mut acc, var), scalar| { + let scalar_var = self.init_var(u256_string(fe_to_u256::(&scalar)), None); + acc.extend(scalar_var.0); + let (lines, out) = + self.init_var(format!("mulmod({var}, {}, r)", scalar_var.1), None); + acc.extend(lines); + (acc, out) + }, + ) + } + + /// Resolve an instance query either from proof evals or local + /// interpolation. + fn instance_eval_at(&self, column_index: usize, rotation: i32) -> String { + if column_index < self.meta.num_committed_instances { + self.data + .committed_instance_evals + .get(&(column_index, rotation)) + .expect("committed instance eval present") + .to_string() + } else { + // The current public API supports one non-committed instance + // column, whose Lagrange-combined evaluation is computed by + // the template prologue and stored at INSTANCE_EVAL_MPTR. + self.data.instance_eval.to_string() + } + } + + /// Bind a Yul expression to a local variable, reusing cached variables. + fn init_var(&self, value: impl ToString, var: Option) -> (Vec, String) { + let value = value.to_string(); + if self.var_cache.borrow().contains_key(&value) { + (vec![], self.var_cache.borrow()[&value].clone()) + } else { + let var = var.unwrap_or_else(|| self.next_var()); + self.var_cache.borrow_mut().insert(value.clone(), var.clone()); + (vec![format!("let {var} := {value}")], var) + } + } + + /// Return the next `varN` local name. + fn next_var(&self) -> String { + let count = *self.var_counter.borrow(); + *self.var_counter.borrow_mut() += 1; + format!("var{count}") + } +} + +/// Render a `U256` as the shortest stable hexadecimal Yul literal. +fn u256_string(value: U256) -> String { + if value.bit_len() < 64 { + format!("0x{:x}", value.as_limbs()[0]) + } else { + format!("0x{value:x}") + } +} + +/// Stable variable name for a column evaluation and rotation. +fn column_eval_var(prefix: &'static str, column_index: usize, rotation: i32) -> String { + match rotation.cmp(&0) { + Ordering::Less => format!("{prefix}_{column_index}_prev_{}", rotation.abs()), + Ordering::Equal => format!("{prefix}_{column_index}"), + Ordering::Greater => format!("{prefix}_{column_index}_next_{rotation}"), + } +} + +/// Flatten a product tree into leaf expressions. +fn collect_product_factors<'a>( + expression: &'a Expression, + factors: &mut Vec<&'a Expression>, +) { + if let Expression::Product(lhs, rhs) = expression { + collect_product_factors(lhs, factors); + collect_product_factors(rhs, factors); + } else { + factors.push(expression); + } +} diff --git a/proofs/solidity-verifier/src/lowering/render/mod.rs b/proofs/solidity-verifier/src/lowering/render/mod.rs new file mode 100644 index 000000000..93a0d5b59 --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/render/mod.rs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Rendering data models and Yul formatting hooks. +//! +//! Rendering is deliberately the last step: callers pass already-planned +//! calldata, layout, VK, KZG, and quotient facts into simple Askama models so +//! templates stay declarative. + +pub(crate) mod models; +pub(crate) mod yul; + +pub(crate) use models::*; diff --git a/proofs/solidity-verifier/src/lowering/render/models.rs b/proofs/solidity-verifier/src/lowering/render/models.rs new file mode 100644 index 000000000..8d66f8c7f --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/render/models.rs @@ -0,0 +1,1369 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Askama data models for the generated Solidity/Yul templates. +//! +//! The structs in this module are deliberately plain: they carry +//! already-planned calldata, memory, VK, quotient, and PCS facts into the +//! templates. Keeping the planning logic outside Askama makes generated +//! Solidity easier to audit and lets Rust tests validate layouts before +//! rendering. + +use std::fmt; + +use askama::{Error, Template}; +use ruint::aliases::U256; + +use crate::lowering::{ + abi::proof::ProofCalldataLayout, + encoding::Ptr, + layout, + layout::{ + memory::{VerifierMemoryLayout, VkConstructorMemoryLayout, G1_BYTES, WORD_BYTES}, + vk_payload::{PayloadSectionKind, VkPayloadLayout}, + }, +}; + +/// G1 point in EIP-2537 padded encoding: (x_hi, x_lo, y_hi, y_lo). +pub(crate) type G1Words = (U256, U256, U256, U256); + +#[derive(Clone, Copy, Debug)] +pub(crate) struct TemplateConstants { + /// EVM word byte width. + pub(crate) word_bytes: usize, + /// EIP-2537 padded G1 byte width. + pub(crate) g1_bytes: usize, + /// G1MSM input tuple byte width. + pub(crate) g1_msm_pair_bytes: usize, + /// G1ADD input byte width. + pub(crate) g1add_input_bytes: usize, + /// Pairing input byte width for one pair. + pub(crate) pairing_pair_bytes: usize, + /// Pairing input byte width for two pairs. + pub(crate) pairing_two_pair_bytes: usize, + /// EIP-2537 precompile constants. + pub(crate) eip2537: Eip2537TemplateConstants, + /// EIP-198 modexp constants. + pub(crate) modexp: ModexpTemplateConstants, + /// Public accumulator layout constants. + pub(crate) accumulator: AccumulatorTemplateConstants, + /// Compact quotient VM constants. + pub(crate) quotient_vm: QuotientVmTemplateConstants, +} + +/// EIP-2537 precompile addresses and gas formulas rendered into templates. +#[derive(Clone, Copy, Debug)] +pub(crate) struct Eip2537TemplateConstants { + pub(crate) g1add_address: usize, + pub(crate) g1msm_address: usize, + pub(crate) pairing_address: usize, + pub(crate) g1add_gas_cap: usize, + pub(crate) g1msm_smoke_gas_cap: usize, + pub(crate) pairing_smoke_gas_cap: usize, + pub(crate) smoke_scratch_bytes: usize, +} + +/// EIP-198 modexp frame constants rendered into templates. +#[derive(Clone, Copy, Debug)] +pub(crate) struct ModexpTemplateConstants { + pub(crate) address: usize, + pub(crate) frame_bytes: usize, + pub(crate) output_bytes: usize, + pub(crate) base_len_offset: usize, + pub(crate) exp_len_offset: usize, + pub(crate) mod_len_offset: usize, + pub(crate) base_offset: usize, + pub(crate) exp_offset: usize, + pub(crate) mod_offset: usize, +} + +/// Public accumulator ABI and pairing-batch constants. +#[derive(Clone, Copy, Debug)] +pub(crate) struct AccumulatorTemplateConstants { + pub(crate) limbs_per_word: usize, + pub(crate) pairing_batch_domain_tag_hex: &'static str, + pub(crate) pairing_batch_rhs_offset: usize, + pub(crate) pairing_batch_lhs_offset: usize, + pub(crate) pairing_batch_acc_rhs_offset: usize, + pub(crate) pairing_batch_acc_lhs_offset: usize, + pub(crate) pairing_batch_hash_bytes: usize, +} + +/// Compact quotient VM opcode constants rendered into Solidity/Yul. +#[derive(Clone, Copy, Debug)] +pub(crate) struct QuotientVmOpcodeTemplateConstants { + pub(crate) push_const: u8, + pub(crate) push_mem_literal: u8, + pub(crate) push_mem_token: u8, + pub(crate) push_mem_token_offset: u8, + pub(crate) push_mem_u16: u8, + pub(crate) add: u8, + pub(crate) mul: u8, + pub(crate) neg: u8, + pub(crate) push_const_u8: u8, + pub(crate) fold_main: u8, + pub(crate) fold_selector: u8, + pub(crate) add_const_u8: u8, + pub(crate) mul_const_u8: u8, + pub(crate) add_const: u8, + pub(crate) mul_const: u8, + pub(crate) add_mem_u16: u8, + pub(crate) mul_mem_u16: u8, + pub(crate) add_mul_mem_mem_const_u8: u8, + pub(crate) add_mul_const_u8_mem_u16: u8, + pub(crate) add_mul_mem_mem: u8, + pub(crate) run_add_mul_mem_mem_const_u8: u8, + pub(crate) run_add_mul_const_u8_mem_u16: u8, + pub(crate) affine_sum: u8, + pub(crate) native_permutation: u8, + pub(crate) native_lookup: u8, + pub(crate) native_identity: u8, + pub(crate) lin7: u8, + pub(crate) bilin7_row: u8, + pub(crate) bilin7_pairwise: u8, + pub(crate) modarith7: u8, + pub(crate) pow5: u8, +} + +/// Compact quotient VM memory-token constants rendered into Solidity/Yul. +#[derive(Clone, Copy, Debug)] +pub(crate) struct QuotientVmMemTokenTemplateConstants { + pub(crate) l0: u8, + pub(crate) l_last: u8, + pub(crate) l_blind: u8, + pub(crate) beta: u8, + pub(crate) gamma: u8, + pub(crate) x: u8, + pub(crate) theta: u8, + pub(crate) trash_challenge: u8, + pub(crate) instance_eval: u8, +} + +/// Compact quotient VM table-level constants rendered into templates. +#[derive(Clone, Copy, Debug)] +pub(crate) struct QuotientVmTemplateConstants { + pub(crate) op: QuotientVmOpcodeTemplateConstants, + pub(crate) mem: QuotientVmMemTokenTemplateConstants, + pub(crate) byte_u32_bytes: usize, + pub(crate) limb_count: usize, + pub(crate) limb_pairwise_coeffs: usize, +} + +impl Default for TemplateConstants { + /// Build template constants from the Rust-side layout and VM specs. + fn default() -> Self { + use crate::lowering::quotient_numerator::vm as q; + + Self { + word_bytes: layout::WORD_BYTES, + g1_bytes: layout::G1_BYTES, + g1_msm_pair_bytes: layout::G1_MSM_PAIR_BYTES, + g1add_input_bytes: layout::G1ADD_INPUT_BYTES, + pairing_pair_bytes: layout::PAIRING_PAIR_BYTES, + pairing_two_pair_bytes: layout::PAIRING_TWO_PAIR_BYTES, + eip2537: Eip2537TemplateConstants { + g1add_address: layout::precompile::G1ADD_ADDRESS, + g1msm_address: layout::precompile::G1MSM_ADDRESS, + pairing_address: layout::precompile::PAIRING_ADDRESS, + g1add_gas_cap: layout::precompile::G1ADD_GAS_CAP, + g1msm_smoke_gas_cap: layout::precompile::G1MSM_SMOKE_GAS_CAP, + pairing_smoke_gas_cap: layout::precompile::PAIRING_SMOKE_GAS_CAP, + smoke_scratch_bytes: layout::PAIRING_PAIR_BYTES, + }, + modexp: ModexpTemplateConstants { + address: layout::precompile::MODEXP_ADDRESS, + frame_bytes: layout::MODEXP_FRAME_BYTES, + output_bytes: layout::WORD_BYTES, + base_len_offset: layout::modexp_frame::BASE_LEN_OFFSET, + exp_len_offset: layout::modexp_frame::EXP_LEN_OFFSET, + mod_len_offset: layout::modexp_frame::MOD_LEN_OFFSET, + base_offset: layout::modexp_frame::BASE_OFFSET, + exp_offset: layout::modexp_frame::EXP_OFFSET, + mod_offset: layout::modexp_frame::MOD_OFFSET, + }, + accumulator: AccumulatorTemplateConstants { + limbs_per_word: layout::accumulator::LIMBS_PER_WORD, + pairing_batch_domain_tag_hex: layout::accumulator::PAIRING_BATCH_DOMAIN_TAG_HEX, + pairing_batch_rhs_offset: layout::accumulator::PAIRING_BATCH_RHS_OFFSET, + pairing_batch_lhs_offset: layout::accumulator::PAIRING_BATCH_LHS_OFFSET, + pairing_batch_acc_rhs_offset: layout::accumulator::PAIRING_BATCH_ACC_RHS_OFFSET, + pairing_batch_acc_lhs_offset: layout::accumulator::PAIRING_BATCH_ACC_LHS_OFFSET, + pairing_batch_hash_bytes: layout::accumulator::PAIRING_BATCH_HASH_BYTES, + }, + quotient_vm: QuotientVmTemplateConstants { + op: QuotientVmOpcodeTemplateConstants { + push_const: q::Q_OP_PUSH_CONST, + push_mem_literal: q::Q_OP_PUSH_MEM_LITERAL, + push_mem_token: q::Q_OP_PUSH_MEM_TOKEN, + push_mem_token_offset: q::Q_OP_PUSH_MEM_TOKEN_OFFSET, + push_mem_u16: q::Q_OP_PUSH_MEM_U16, + add: q::Q_OP_ADD, + mul: q::Q_OP_MUL, + neg: q::Q_OP_NEG, + push_const_u8: q::Q_OP_PUSH_CONST_U8, + fold_main: q::Q_OP_FOLD_MAIN, + fold_selector: q::Q_OP_FOLD_SELECTOR, + add_const_u8: q::Q_OP_ADD_CONST_U8, + mul_const_u8: q::Q_OP_MUL_CONST_U8, + add_const: q::Q_OP_ADD_CONST, + mul_const: q::Q_OP_MUL_CONST, + add_mem_u16: q::Q_OP_ADD_MEM_U16, + mul_mem_u16: q::Q_OP_MUL_MEM_U16, + add_mul_mem_mem_const_u8: q::Q_OP_ADD_MUL_MEM_MEM_CONST_U8, + add_mul_const_u8_mem_u16: q::Q_OP_ADD_MUL_CONST_U8_MEM_U16, + add_mul_mem_mem: q::Q_OP_ADD_MUL_MEM_MEM, + run_add_mul_mem_mem_const_u8: q::Q_OP_RUN_ADD_MUL_MEM_MEM_CONST_U8, + run_add_mul_const_u8_mem_u16: q::Q_OP_RUN_ADD_MUL_CONST_U8_MEM_U16, + affine_sum: q::Q_OP_AFFINE_SUM, + native_permutation: q::Q_OP_NATIVE_PERMUTATION, + native_lookup: q::Q_OP_NATIVE_LOOKUP, + native_identity: q::Q_OP_NATIVE_IDENTITY, + lin7: q::Q_OP_LIN7, + bilin7_row: q::Q_OP_BILIN7_ROW, + bilin7_pairwise: q::Q_OP_BILIN7_PAIRWISE, + modarith7: q::Q_OP_MODARITH7, + pow5: q::Q_OP_POW5, + }, + mem: QuotientVmMemTokenTemplateConstants { + l0: q::Q_MEM_L0, + l_last: q::Q_MEM_L_LAST, + l_blind: q::Q_MEM_L_BLIND, + beta: q::Q_MEM_BETA, + gamma: q::Q_MEM_GAMMA, + x: q::Q_MEM_X, + theta: q::Q_MEM_THETA, + trash_challenge: q::Q_MEM_TRASH_CHALLENGE, + instance_eval: q::Q_MEM_INSTANCE_EVAL, + }, + byte_u32_bytes: q::QUOTIENT_VM_BYTE_U32_BYTES, + limb_count: q::QUOTIENT_VM_LIMBS, + limb_pairwise_coeffs: q::QUOTIENT_VM_PAIRWISE_COEFFS, + }, + } + } +} + +#[derive(Clone, Debug, Template)] +#[template(path = "contracts/Halo2VerifyingKey.sol")] +pub(crate) struct Halo2VerifyingKey { + /// Constructor memory base used while returning VK bytes. + pub(crate) constructor_payload_mptr: usize, + /// Header constants followed by optional quotient constants/program words. + pub(crate) constants: Vec<(&'static str, U256)>, + /// EIP-2537-padded fixed commitments. + pub(crate) fixed_comms: Vec, + /// EIP-2537-padded permutation commitments. + pub(crate) permutation_comms: Vec, + /// Word offset of quotient constants, when present. + pub(crate) quotient_const_offset_words: Option, + /// Number of quotient constant words. + pub(crate) quotient_const_words: usize, + /// Word offset of quotient program words, when present. + pub(crate) quotient_program_offset_words: Option, + /// Number of quotient program words. + pub(crate) quotient_program_words: usize, +} + +/// INVALID byte that prefixes the separate VK runtime payload. +pub(crate) const VK_RUNTIME_PREFIX: u8 = 0xfe; +/// Number of bytes skipped before copying the separate VK payload. +pub(crate) const VK_RUNTIME_PREFIX_LEN: usize = 1; + +impl Halo2VerifyingKey { + /// Reconstruct and validate the typed VK payload layout. + pub(crate) fn payload_layout(&self) -> Result { + let quotient_words = self.quotient_const_words + self.quotient_program_words; + let header_words = self.constants.len().checked_sub(quotient_words).ok_or_else(|| { + format!( + "VK constant table too short: constants={} quotient_words={quotient_words}", + self.constants.len() + ) + })?; + let layout = VkPayloadLayout::for_vk( + header_words, + self.quotient_const_words, + self.quotient_program_words, + self.fixed_comms.len(), + self.permutation_comms.len(), + )?; + + if let Some(offset) = self.quotient_const_offset_words { + let expected = layout.word_offset(PayloadSectionKind::QuotientConstants)?; + if offset != expected { + return Err(format!( + "quotient const offset mismatch: got {offset:#x}, expected {expected:#x}" + )); + } + } + if let Some(offset) = self.quotient_program_offset_words { + let expected = layout.word_offset(PayloadSectionKind::QuotientProgram)?; + if offset != expected { + return Err(format!( + "quotient program offset mismatch: got {offset:#x}, expected {expected:#x}" + )); + } + } + + Ok(layout) + } + + /// Validate that typed layout length equals rendered byte length. + pub(crate) fn validate_payload_layout(&self) -> Result<(), String> { + let layout = self.payload_layout()?; + if layout.total_bytes() != self.len() { + return Err(format!( + "VK payload byte length mismatch: layout={} bytes rendered={} bytes", + layout.total_bytes(), + self.len() + )); + } + let constructor_memory = VkConstructorMemoryLayout::new(self.runtime_len()); + constructor_memory.validate()?; + if self.constructor_payload_mptr != constructor_memory.payload_mptr { + return Err(format!( + "VK constructor payload pointer mismatch: got {:#x}, expected {:#x}", + self.constructor_payload_mptr, constructor_memory.payload_mptr + )); + } + Ok(()) + } + + /// Rendered VK payload length in bytes. + pub(crate) fn len(&self) -> usize { + // 32 bytes per scalar constant + 128 bytes per G1 point (EIP-2537 padded). + (self.constants.len() * WORD_BYTES) + + (self.fixed_comms.len() + self.permutation_comms.len()) * G1_BYTES + } + + /// Deployed VK runtime length in bytes. + pub(crate) fn runtime_len(&self) -> usize { + VK_RUNTIME_PREFIX_LEN + self.len() + } + + /// Rendered VK payload bytes in the exact contract return order. + pub(crate) fn bytes(&self) -> Vec { + self.constants + .iter() + .map(|(_, value)| *value) + .chain(self.fixed_comms.iter().flat_map(|(a, b, c, d)| [*a, *b, *c, *d])) + .chain(self.permutation_comms.iter().flat_map(|(a, b, c, d)| [*a, *b, *c, *d])) + .flat_map(|value| value.to_be_bytes::<32>()) + .collect() + } + + /// Deployed VK runtime bytes. Byte 0 is an unconditional INVALID opcode; + /// the payload copied by the verifier starts at byte 1. + pub(crate) fn runtime_bytes(&self) -> Vec { + let mut runtime = Vec::with_capacity(self.runtime_len()); + runtime.push(VK_RUNTIME_PREFIX); + runtime.extend(self.bytes()); + runtime + } +} + +/// Per-user-phase summary: how many advice commitments to absorb in this +/// phase, how many challenges to squeeze afterwards, and the index of +/// the first challenge within `CHALLENGE_MPTR[..]`. +#[derive(Clone, Copy, Debug)] +pub(crate) struct UserPhase { + pub(crate) advice_bytes: usize, + pub(crate) num_challenges: usize, + /// Starting offset (in 32-byte words) into the CHALLENGE_MPTR area + /// where this phase's challenges should be written. + pub(crate) challenge_offset: usize, +} + +#[derive(Clone, Debug)] +pub(crate) struct VerifierCodegenLayout { + /// Planned proof calldata layout. + pub(crate) proof: ProofCalldataLayout, +} + +/// Word offsets for VK header fields rendered as template constants. +#[derive(Clone, Copy, Debug)] +pub(crate) struct VkHeaderTemplateSlots { + pub(crate) vk_digest: usize, + pub(crate) num_instances: usize, + pub(crate) k: usize, + pub(crate) n_inv: usize, + pub(crate) omega: usize, + pub(crate) omega_inv: usize, + pub(crate) omega_inv_to_l: usize, + pub(crate) has_accumulator: usize, + pub(crate) acc_offset: usize, + pub(crate) num_acc_limbs: usize, + pub(crate) num_acc_limb_bits: usize, + pub(crate) g1_base: usize, + pub(crate) g2_base: usize, + pub(crate) neg_s_g2_base: usize, +} + +impl Default for VkHeaderTemplateSlots { + /// Build slot offsets from the typed VK header schema. + fn default() -> Self { + use crate::lowering::layout::{VkHeaderLayout, VkHeaderSlot as Slot}; + + Self { + vk_digest: VkHeaderLayout::field(Slot::VkDigest).slot.word(), + num_instances: VkHeaderLayout::field(Slot::NumInstances).slot.word(), + k: VkHeaderLayout::field(Slot::K).slot.word(), + n_inv: VkHeaderLayout::field(Slot::NInv).slot.word(), + omega: VkHeaderLayout::field(Slot::Omega).slot.word(), + omega_inv: VkHeaderLayout::field(Slot::OmegaInv).slot.word(), + omega_inv_to_l: VkHeaderLayout::field(Slot::OmegaInvToL).slot.word(), + has_accumulator: VkHeaderLayout::field(Slot::HasAccumulator).slot.word(), + acc_offset: VkHeaderLayout::field(Slot::AccOffset).slot.word(), + num_acc_limbs: VkHeaderLayout::field(Slot::NumAccLimbs).slot.word(), + num_acc_limb_bits: VkHeaderLayout::field(Slot::NumAccLimbBits).slot.word(), + g1_base: VkHeaderLayout::field(Slot::G1Base).slot.word(), + g2_base: VkHeaderLayout::field(Slot::G2Base).slot.word(), + neg_s_g2_base: VkHeaderLayout::field(Slot::NegSG2Base).slot.word(), + } + } +} + +#[derive(Template)] +#[template(path = "contracts/Halo2Verifier.sol")] +pub(crate) struct Halo2Verifier { + pub(crate) template_constants: TemplateConstants, + pub(crate) trace: bool, + /// When true, the rendered verifier emits LOG1 gas() checkpoints at + /// section boundaries. See SOLIDITY_GAS_CHECKPOINTS_ENABLED. + pub(crate) gas_checkpoints: bool, + pub(crate) quotient_pow5_helper: bool, + pub(crate) quotient_limb7_helper: bool, + pub(crate) quotient_wide_limb7_helper: bool, + /// Generated gas cap for one-pair G1MSM calls. + pub(crate) g1msm_single_gas_cap: usize, + /// Generated gas cap for the trace-only linearization MSM. + pub(crate) lin_trace_g1msm_gas_cap: usize, + /// Generated max gas cap for the accumulator RHS MSM. + pub(crate) acc_rhs_g1msm_gas_cap: usize, + /// Generated gas cap for the final two-pair KZG pairing check. + pub(crate) final_pairing_gas_cap: usize, + pub(crate) limb7_yul_coeffs: [&'static str; layout::quotient_limb::LIN_COEFFS], + pub(crate) wide_limb7_yul_coeffs: [&'static str; layout::quotient_limb::LIN_COEFFS], + pub(crate) fr_delta: String, + pub(crate) embedded_vk: Option, + pub(crate) expected_vk_codehash: Option, + pub(crate) vk_len: usize, + /// Generated public-instance count for this pinned VK/proof layout. + pub(crate) num_instances: usize, + /// Generated evaluation domain size exponent. + pub(crate) k: usize, + pub(crate) proof_len: usize, + pub(crate) codegen_layout: VerifierCodegenLayout, + pub(crate) memory: VerifierMemoryLayout, + pub(crate) vk_header: VkHeaderTemplateSlots, + pub(crate) vk_mptr: Ptr, + pub(crate) challenge_mptr: Ptr, + pub(crate) theta_mptr: Ptr, + pub(crate) proof_cptr: Ptr, + pub(crate) abi_selector_bytes: usize, + pub(crate) abi_proof_head_offset: usize, + pub(crate) abi_instances_head_cptr: usize, + /// Calldata byte offset of the `num_instances` length-prefix word + /// that ABI-encodes the `instances` array. Equals + /// `proof_cptr + proof_len` (in bytes). Materialised as a separate + /// field because this is an ABI byte offset used directly by the + /// hand-rolled calldata parser, while `Ptr` values are word-oriented. + pub(crate) num_instance_cptr: usize, + /// Calldata byte offset of the first instance value (immediately + /// after `num_instance_cptr`). + pub(crate) instance_cptr: usize, + pub(crate) quotient_comm_cptr: Ptr, + pub(crate) num_neg_lagranges: usize, + /// Per-user-phase advice + user-challenge counts (excludes theta). + pub(crate) user_phases: Vec, + pub(crate) num_user_challenges: usize, + pub(crate) num_lookups: usize, + pub(crate) num_permutation_zs: usize, + pub(crate) lookup_h_plus_acc: usize, + pub(crate) num_trashcans: usize, + pub(crate) num_quotients: usize, + pub(crate) num_evals: usize, + /// Word offset (relative to memory base) of the first EIP-2537-padded + /// advice commitment. The remaining categories (lookup_m, perm_z, + /// lookup_helper, lookup_z, trashcan, quotient_limb) are laid out + /// contiguously after this base with a 4-word stride per G1. + pub(crate) comms_mptr_base: Ptr, + /// Memory base of the decoded-evals buffer (Optimisation H3). + /// The transcript-side `evaluations` loop spills the decoded scalar value + /// to this buffer so that every later eval reference renders as + /// `mload(...)`. + pub(crate) reversed_evals_mptr: Ptr, + /// Scratch base for simple-selector linearization accumulators. + /// These values are needed only between quotient-eval emission and + /// the linearization MSM, so the region may be reused by later PCS + /// scratch tables. + pub(crate) selector_acc_mptr: usize, + pub(crate) quotient_external: Option, + pub(crate) expected_quotient_len: Option, + pub(crate) expected_quotient_codehash: Option, + pub(crate) quotient_inline_computations: Vec>, + pub(crate) quotient_eval_numer_computations: Vec>, + pub(crate) quotient_post_vm_computations: Vec>, + pub(crate) quotient_native_permutation_computation: Vec, + pub(crate) quotient_native_lookup_computation: Vec, + pub(crate) quotient_native_identity_computations: Vec>, + pub(crate) quotient_program: Option, + pub(crate) pcs_computations: Vec>, + /// Sorted simple-selector fixed-column indices. Each is rendered + /// into a Yul snippet that adds `S_i_com * sel_acc_i` to the + /// linearization commitment after Q_folded is scaled by (1-x^n). + pub(crate) simple_selector_cols: Vec, + pub(crate) proof_commit_trace_base: usize, + pub(crate) proof_eval_trace_base: usize, + pub(crate) quotient_identity_trace_base: u64, + pub(crate) selector_trace_base: usize, + /// Memory pointer base for the embedded VK fixed commitments. Used + /// to resolve per-column G1 offsets in the simple-selector MSM. + pub(crate) fixed_comm_mptr: usize, + /// When true, mirrors midnight-proofs/truncated-challenges: + /// - x3 is masked to 128 bits immediately after squeeze + /// - x1 / x4 powers are masked to 128 bits at use, with the internal + /// full-precision accumulator preserved + /// + /// Driven by `cfg!(feature = "truncated-challenges")` in + /// `SolidityGenerator::generate_verifier`. + pub(crate) truncated_challenges: bool, + /// Accumulator metadata expected by this generated verifier. These + /// constants mirror the generator-side `AccumulatorEncoding` and are + /// checked against the VK header after `extcodecopy` / embedded VK + /// materialization, so a stale or mismatched VK fails before public-input + /// decoding chooses the wrong schema. + pub(crate) expected_has_accumulator: bool, + pub(crate) expected_acc_offset: usize, + pub(crate) expected_num_acc_limbs: usize, + pub(crate) expected_num_acc_limb_bits: usize, + /// Whether the public accumulator payload carries explicit LHS/RHS scalar + /// words. Moonlight wrap proofs expose an already collapsed point pair and + /// use implicit unit scalars instead. + pub(crate) expected_acc_has_carried_scalars: bool, + /// Fixed bases serialized by `AssignedAccumulator::as_public_input` + /// for the RHS accumulator MSM, in the exact lexicographic + /// `BTreeMap` order used by midnight-circuits. This is the public + /// accumulator's fixed-scalar width, not necessarily every fixed + /// commitment stored in the VK contract. Each tuple is + /// `(point_mptr, negate_scalar)`: `-G` is represented as the + /// regular generator with the scalar negated modulo Fr. + pub(crate) acc_fixed_bases: Vec<(usize, bool)>, +} + +#[derive(Clone, Debug)] +pub(crate) struct QuotientExternal { + /// Base of the copied frame supplied to the external evaluator. + pub(crate) frame_base: usize, + /// Length of the copied frame in bytes. + pub(crate) frame_len: usize, + /// Expected return payload length in bytes. + pub(crate) output_len: usize, + /// Magic version tag checked between verifier and evaluator. + pub(crate) magic: u64, +} + +impl QuotientExternal { + /// End byte of the copied frame, exclusive. + fn frame_end(&self) -> usize { + self.frame_base + self.frame_len + } + + /// Return whether a range is fully contained by the external frame. + fn contains_range(&self, start: usize, len: usize) -> bool { + let end = start.saturating_add(len); + start >= self.frame_base && end <= self.frame_end() + } + + /// Return whether a range is disjoint from the copied external frame. + fn disjoint_range(&self, start: usize, len: usize) -> bool { + let end = start.saturating_add(len); + end <= self.frame_base || start >= self.frame_end() + } + + /// Validate that a named range is fully copied into the external frame. + fn validate_contains(&self, name: &str, start: usize, len: usize) -> Result<(), String> { + if self.contains_range(start, len) { + Ok(()) + } else { + Err(format!( + "external quotient frame does not contain {name}: range {start:#x}..{:#x}, frame {:#x}..{:#x}", + start.saturating_add(len), + self.frame_base, + self.frame_end() + )) + } + } +} + +#[derive(Template)] +#[template(path = "contracts/Halo2QuotientEvaluator.sol")] +pub(crate) struct Halo2QuotientEvaluator { + pub(crate) template_constants: TemplateConstants, + pub(crate) trace: bool, + pub(crate) quotient_pow5_helper: bool, + pub(crate) quotient_limb7_helper: bool, + pub(crate) quotient_wide_limb7_helper: bool, + pub(crate) limb7_yul_coeffs: [&'static str; layout::quotient_limb::LIN_COEFFS], + pub(crate) wide_limb7_yul_coeffs: [&'static str; layout::quotient_limb::LIN_COEFFS], + pub(crate) fr_delta: String, + pub(crate) memory: VerifierMemoryLayout, + pub(crate) vk_mptr: Ptr, + pub(crate) vk_len: usize, + pub(crate) challenge_mptr: Ptr, + pub(crate) num_user_challenges: usize, + pub(crate) theta_mptr: Ptr, + pub(crate) reversed_evals_mptr: Ptr, + pub(crate) num_evals: usize, + pub(crate) quotient_external: QuotientExternal, + pub(crate) quotient_inline_computations: Vec>, + pub(crate) quotient_eval_numer_computations: Vec>, + pub(crate) quotient_post_vm_computations: Vec>, + pub(crate) quotient_native_permutation_computation: Vec, + pub(crate) quotient_native_lookup_computation: Vec, + pub(crate) quotient_native_identity_computations: Vec>, + pub(crate) quotient_program: Option, + pub(crate) simple_selector_cols: Vec, + pub(crate) quotient_identity_trace_base: u64, +} + +#[derive(Clone, Debug)] +pub(crate) struct QuotientProgram { + /// Program length in bytes before word padding. + pub(crate) len: usize, + /// Opcode switch arms required by this generated program. + pub(crate) op_usage: QuotientVmOpcodeUsage, + /// Memory-token switch arms required by this generated program. + pub(crate) mem_usage: QuotientVmMemUsage, + /// Memory pointer to the first constant word. + pub(crate) const_mptr: usize, + /// Persistent quotient numerator accumulator word. + pub(crate) eval_numer_mptr: usize, + /// Persistent trace-id word. + pub(crate) trace_id_mptr: usize, + /// First word of the optional selector `y^k` power table. + pub(crate) selector_power_mptr: usize, + /// Highest selector gap/tail exponent referenced by generated code. + pub(crate) selector_max_power: usize, + /// Non-zero final selector y-power tail updates. + pub(crate) selector_tail_updates: Vec, + /// Operand stack / callback scratch base. + pub(crate) stack_mptr: usize, + /// Memory pointer to the first encoded program word. + pub(crate) program_mptr: usize, +} + +#[derive(Clone, Copy, Debug)] +pub(crate) struct QuotientSelectorTail { + /// Byte offset of the selector bucket under `SELECTOR_ACC_MPTR`. + pub(crate) selector_offset: usize, + /// Byte offset of the tail power under `selector_power_mptr`. + pub(crate) power_offset: usize, +} + +#[derive(Clone, Copy, Debug, Default)] +pub(crate) struct QuotientVmOpcodeUsage { + pub(crate) push_const: bool, + pub(crate) push_mem_literal: bool, + pub(crate) push_mem_token: bool, + pub(crate) push_mem_token_offset: bool, + pub(crate) push_mem_u16: bool, + pub(crate) add: bool, + pub(crate) mul: bool, + pub(crate) neg: bool, + pub(crate) push_const_u8: bool, + pub(crate) fold_main: bool, + pub(crate) fold_selector: bool, + pub(crate) add_const_u8: bool, + pub(crate) mul_const_u8: bool, + pub(crate) add_const: bool, + pub(crate) mul_const: bool, + pub(crate) add_mem_u16: bool, + pub(crate) mul_mem_u16: bool, + pub(crate) add_mul_mem_mem_const_u8: bool, + pub(crate) add_mul_const_u8_mem_u16: bool, + pub(crate) add_mul_mem_mem: bool, + pub(crate) run_add_mul_mem_mem_const_u8: bool, + pub(crate) run_add_mul_const_u8_mem_u16: bool, + pub(crate) affine_sum: bool, + pub(crate) native_permutation: bool, + pub(crate) native_lookup: bool, + pub(crate) native_identity: bool, + pub(crate) lin7: bool, + pub(crate) bilin7_row: bool, + pub(crate) bilin7_pairwise: bool, + pub(crate) modarith7: bool, + pub(crate) pow5: bool, +} + +#[derive(Clone, Copy, Debug, Default)] +pub(crate) struct QuotientVmMemUsage { + pub(crate) l0: bool, + pub(crate) l_last: bool, + pub(crate) l_blind: bool, + pub(crate) beta: bool, + pub(crate) gamma: bool, + pub(crate) x: bool, + pub(crate) theta: bool, + pub(crate) trash_challenge: bool, + pub(crate) instance_eval: bool, +} + +impl Halo2VerifyingKey { + /// Render the verifying-key contract. + pub(crate) fn render(&self, writer: &mut impl fmt::Write) -> Result<(), fmt::Error> { + self.render_into(writer).map_err(|err| match err { + Error::Fmt(err) => err, + _ => unreachable!(), + }) + } +} + +impl Halo2Verifier { + /// Validate template inputs against the typed proof and memory layouts. + pub(crate) fn validate_layout(&self) -> Result<(), String> { + self.memory.validate()?; + + let proof_cptr = self.proof_cptr.value().as_usize(); + let proof_layout = &self.codegen_layout.proof; + if proof_layout.proof_cptr != proof_cptr { + return Err(format!( + "proof calldata layout mismatch: template proof_cptr({proof_cptr:#x}) != layout proof_cptr({:#x})", + proof_layout.proof_cptr + )); + } + if proof_layout.proof_len != self.proof_len { + return Err(format!( + "proof length mismatch: template {:#x} != layout {:#x}", + self.proof_len, proof_layout.proof_len + )); + } + if proof_layout.proof_end != self.num_instance_cptr { + return Err(format!( + "proof calldata layout mismatch: proof_end({:#x}) != num_instance_cptr({:#x})", + proof_layout.proof_end, self.num_instance_cptr + )); + } + if self.num_instance_cptr + WORD_BYTES != self.instance_cptr { + return Err(format!( + "instance calldata layout mismatch: num_instance_cptr({:#x}) + 0x20 != instance_cptr({:#x})", + self.num_instance_cptr, self.instance_cptr + )); + } + let vk_end = self.vk_mptr.value().as_usize() + self.vk_len; + let challenge_mptr = self.challenge_mptr.value().as_usize(); + if vk_end > challenge_mptr { + return Err(format!( + "VK memory layout mismatch: VK_MPTR({:#x}) + vk_len({:#x}) overlaps challenge_mptr({challenge_mptr:#x})", + self.vk_mptr.value().as_usize(), + self.vk_len + )); + } + + let expected_quotient_cptr = proof_layout.quotient_comm_cptr; + let quotient_cptr = self.quotient_comm_cptr.value().as_usize(); + if quotient_cptr != expected_quotient_cptr { + return Err(format!( + "quotient commitment calldata mismatch: got {quotient_cptr:#x}, expected {expected_quotient_cptr:#x}" + )); + } + + let expected_proof_len = proof_layout.proof_len; + if self.proof_len != expected_proof_len { + return Err(format!( + "proof length mismatch: got {:#x}, expected {expected_proof_len:#x}", + self.proof_len + )); + } + + let comms_base = self.comms_mptr_base.value().as_usize(); + let committed_g1s = proof_layout.commitment_g1_count(); + let expected_selector_acc = + (comms_base + committed_g1s * G1_BYTES).next_multiple_of(WORD_BYTES); + if self.selector_acc_mptr != expected_selector_acc { + return Err(format!( + "selector accumulator layout mismatch: got {:#x}, expected {expected_selector_acc:#x}", + self.selector_acc_mptr + )); + } + + if self.quotient_external.is_none() + && self.quotient_program.is_none() + && self.quotient_eval_numer_computations.is_empty() + { + return Err( + "non-external quotient rendering requires a compact quotient program or legacy direct quotient blocks" + .to_string(), + ); + } + + if let Some(qext) = &self.quotient_external { + qext.validate_contains("VK payload", self.vk_mptr.value().as_usize(), self.vk_len)?; + qext.validate_contains( + "user challenge block", + self.challenge_mptr.value().as_usize(), + self.num_user_challenges * WORD_BYTES, + )?; + let quotient_input_end = self.memory.instance_eval_mptr.value().as_usize() + WORD_BYTES; + qext.validate_contains( + "quotient challenge/common slots", + self.theta_mptr.value().as_usize(), + quotient_input_end.saturating_sub(self.theta_mptr.value().as_usize()), + )?; + qext.validate_contains( + "decoded proof evaluations", + self.reversed_evals_mptr.value().as_usize(), + self.num_evals * WORD_BYTES, + )?; + + let expected_output_len = 2 * WORD_BYTES + self.simple_selector_cols.len() * WORD_BYTES; + if qext.output_len != expected_output_len { + return Err(format!( + "external quotient output length mismatch: got {:#x}, expected {expected_output_len:#x}", + qext.output_len + )); + } + if !qext.disjoint_range(self.memory.quotient_return_mptr, expected_output_len) { + return Err(format!( + "external quotient output overlaps copied frame: output {:#x}..{:#x}, frame {:#x}..{:#x}", + self.memory.quotient_return_mptr, + self.memory.quotient_return_mptr + expected_output_len, + qext.frame_base, + qext.frame_end() + )); + } + } + Ok(()) + } + + /// Render the verifier contract. + pub(crate) fn render(&self, writer: &mut impl fmt::Write) -> Result<(), fmt::Error> { + self.render_into(writer).map_err(|err| match err { + Error::Fmt(err) => err, + _ => unreachable!(), + }) + } +} + +impl Halo2QuotientEvaluator { + /// Validate the standalone evaluator's copied frame and output memory. + pub(crate) fn validate_layout(&self) -> Result<(), String> { + self.memory.validate()?; + + let qext = &self.quotient_external; + qext.validate_contains("VK payload", self.vk_mptr.value().as_usize(), self.vk_len)?; + qext.validate_contains( + "user challenge block", + self.challenge_mptr.value().as_usize(), + self.num_user_challenges * WORD_BYTES, + )?; + let quotient_input_end = self.memory.instance_eval_mptr.value().as_usize() + WORD_BYTES; + qext.validate_contains( + "quotient challenge/common slots", + self.theta_mptr.value().as_usize(), + quotient_input_end.saturating_sub(self.theta_mptr.value().as_usize()), + )?; + qext.validate_contains( + "decoded proof evaluations", + self.reversed_evals_mptr.value().as_usize(), + self.num_evals * WORD_BYTES, + )?; + + let expected_output_len = 2 * WORD_BYTES + self.simple_selector_cols.len() * WORD_BYTES; + if qext.output_len != expected_output_len { + return Err(format!( + "external quotient output length mismatch: got {:#x}, expected {expected_output_len:#x}", + qext.output_len + )); + } + if !qext.disjoint_range(self.memory.quotient_return_mptr, expected_output_len) { + return Err(format!( + "external quotient output overlaps copied frame: output {:#x}..{:#x}, frame {:#x}..{:#x}", + self.memory.quotient_return_mptr, + self.memory.quotient_return_mptr + expected_output_len, + qext.frame_base, + qext.frame_end() + )); + } + + Ok(()) + } + + /// Render the standalone quotient evaluator contract. + pub(crate) fn render(&self, writer: &mut impl fmt::Write) -> Result<(), fmt::Error> { + self.render_into(writer).map_err(|err| match err { + Error::Fmt(err) => err, + _ => unreachable!(), + }) + } +} + +mod filters { + use std::fmt::LowerHex; + + /// Askama filter that renders a lower-hex value as an even-width Yul + /// literal. + pub fn hex(value: impl LowerHex) -> ::askama::Result { + let value = format!("{value:x}"); + Ok(if value.len() % 2 == 1 { + format!("0x0{value}") + } else { + format!("0x{value}") + }) + } + + /// Askama filter that renders a lower-hex value padded to `pad` nibbles. + pub fn hex_padded(value: impl LowerHex, pad: usize) -> ::askama::Result { + let string = format!("0x{value:0pad$x}"); + if string == "0x0" { + Ok(format!("0x{}", "0".repeat(pad))) + } else { + Ok(string) + } + } +} + +#[cfg(test)] +mod tests { + use ruint::aliases::U256; + + use super::{G1Words, Halo2Verifier, Halo2VerifyingKey, QuotientProgram, VK_RUNTIME_PREFIX}; + use crate::lowering::{ + abi::proof::ProofCalldataLayout, + encoding::{ConstraintSystemMeta, Ptr}, + layout::{ + memory::{ + VerifierMemoryLayout, VerifierMemoryLayoutConfig, VkConstructorMemoryLayout, + G1_BYTES, WORD_BYTES, + }, + vk_payload::PayloadSectionKind, + }, + protocol::{CommitmentRead, ProofReadPlan, ProtocolPlan}, + }; + + /// Synthetic VK model with configurable fixed/permutation commitment + /// counts. + fn synthetic_vk(num_fixed: usize, num_perm: usize) -> Halo2VerifyingKey { + // 11 named scalars + 4 g1 + 8 g2 + 8 neg_s_g2 = 31 entries (the + // exact layout the verifier expects for the Step 6 named + // VK_DIGEST_MPTR / G1_BASE_MPTR / G2_BASE_MPTR / NEG_S_G2_BASE_MPTR + // slots). + let mut constants: Vec<(&'static str, U256)> = vec![ + ("vk_digest", U256::from(0xde_u64)), + ("num_instances", U256::from(1u64)), + ("k", U256::from(8u64)), + ("n_inv", U256::from(0x1234u64)), + ("omega", U256::from(0x5678u64)), + ("omega_inv", U256::from(0x9abcu64)), + ("omega_inv_to_l", U256::from(0xdef0u64)), + ("has_accumulator", U256::from(0u64)), + ("acc_offset", U256::from(0u64)), + ("num_acc_limbs", U256::from(0u64)), + ("num_acc_limb_bits", U256::from(0u64)), + ]; + constants.extend([ + ("g1_x_hi", U256::from(0x10u64)), + ("g1_x_lo", U256::from(0x11u64)), + ("g1_y_hi", U256::from(0x12u64)), + ("g1_y_lo", U256::from(0x13u64)), + ]); + constants.extend([ + ("g2_x_c0_hi", U256::from(0x20u64)), + ("g2_x_c0_lo", U256::from(0x21u64)), + ("g2_x_c1_hi", U256::from(0x22u64)), + ("g2_x_c1_lo", U256::from(0x23u64)), + ("g2_y_c0_hi", U256::from(0x24u64)), + ("g2_y_c0_lo", U256::from(0x25u64)), + ("g2_y_c1_hi", U256::from(0x26u64)), + ("g2_y_c1_lo", U256::from(0x27u64)), + ]); + constants.extend([ + ("neg_s_g2_x_c0_hi", U256::from(0x30u64)), + ("neg_s_g2_x_c0_lo", U256::from(0x31u64)), + ("neg_s_g2_x_c1_hi", U256::from(0x32u64)), + ("neg_s_g2_x_c1_lo", U256::from(0x33u64)), + ("neg_s_g2_y_c0_hi", U256::from(0x34u64)), + ("neg_s_g2_y_c0_lo", U256::from(0x35u64)), + ("neg_s_g2_y_c1_hi", U256::from(0x36u64)), + ("neg_s_g2_y_c1_lo", U256::from(0x37u64)), + ]); + + let fixed_comms: Vec = (0..num_fixed) + .map(|i| { + let base = U256::from(0x40_u64 + i as u64 * 4); + ( + base, + base + U256::from(1u64), + base + U256::from(2u64), + base + U256::from(3u64), + ) + }) + .collect(); + let permutation_comms: Vec = (0..num_perm) + .map(|i| { + let base = U256::from(0x80_u64 + i as u64 * 4); + ( + base, + base + U256::from(1u64), + base + U256::from(2u64), + base + U256::from(3u64), + ) + }) + .collect(); + let constructor_memory = VkConstructorMemoryLayout::new( + constants.len() * WORD_BYTES + (fixed_comms.len() + permutation_comms.len()) * G1_BYTES, + ); + Halo2VerifyingKey { + constructor_payload_mptr: constructor_memory.payload_mptr, + constants, + fixed_comms, + permutation_comms, + quotient_const_offset_words: None, + quotient_const_words: 0, + quotient_program_offset_words: None, + quotient_program_words: 0, + } + } + + /// Synthetic verifier model used by template-layout validation tests. + fn synthetic_verifier() -> Halo2Verifier { + let proof_cptr = crate::lowering::layout::abi::VERIFY_PROOF_PROOF_CPTR; + let total_advices = 2usize; + let num_lookups = 1usize; + let num_permutation_zs = 1usize; + let lookup_helper_chunks_total = 2usize; + let num_trashcans = 1usize; + let num_quotients = 3usize; + let num_evals = 5usize; + let num_point_sets = 2usize; + let non_quotient_g1s = total_advices + + num_lookups + + num_permutation_zs + + lookup_helper_chunks_total + + num_lookups + + num_trashcans; + let proof_len = (non_quotient_g1s + num_quotients + 2) * G1_BYTES + + (num_evals + num_point_sets) * WORD_BYTES; + let comms_mptr_base = 0x2000usize; + let selector_acc_mptr = comms_mptr_base + (non_quotient_g1s + num_quotients) * G1_BYTES; + let memory = VerifierMemoryLayout::new( + &ConstraintSystemMeta::default(), + &synthetic_vk(0, 0), + Ptr::memory(0x1000), + VerifierMemoryLayoutConfig::default(), + ); + let mut proof = ProofReadPlan::default(); + proof.commitments.extend((0..total_advices).map(|_| CommitmentRead::Advice)); + proof + .commitments + .extend((0..num_lookups).map(|_| CommitmentRead::LookupMultiplicity)); + proof + .commitments + .extend((0..num_permutation_zs).map(|_| CommitmentRead::PermutationProduct)); + proof + .commitments + .extend((0..lookup_helper_chunks_total).map(|_| CommitmentRead::LookupHelper)); + proof + .commitments + .extend((0..num_lookups).map(|_| CommitmentRead::LookupAccumulator)); + proof.commitments.extend((0..num_trashcans).map(|_| CommitmentRead::Trash)); + proof.commitments.extend((0..num_quotients).map(|_| CommitmentRead::Quotient)); + let protocol = ProtocolPlan { + num_user_advices: vec![total_advices], + lookup_chunks: vec![lookup_helper_chunks_total], + num_lookups, + num_permutation_zs, + num_trashcans, + num_quotients, + proof, + ..ProtocolPlan::default() + }; + let proof_layout = + ProofCalldataLayout::from_protocol(&protocol, proof_cptr, num_evals, num_point_sets); + + Halo2Verifier { + template_constants: Default::default(), + trace: false, + gas_checkpoints: false, + quotient_pow5_helper: false, + quotient_limb7_helper: false, + quotient_wide_limb7_helper: false, + g1msm_single_gas_cap: crate::lowering::layout::precompile::g1msm_gas_cap( + crate::lowering::layout::G1_MSM_PAIR_BYTES, + ), + lin_trace_g1msm_gas_cap: crate::lowering::layout::precompile::g1msm_gas_cap( + crate::lowering::layout::G1_MSM_PAIR_BYTES, + ), + acc_rhs_g1msm_gas_cap: crate::lowering::layout::precompile::g1msm_gas_cap( + crate::lowering::layout::G1_MSM_PAIR_BYTES, + ), + final_pairing_gas_cap: crate::lowering::layout::precompile::pairing_gas_cap( + crate::lowering::layout::PAIRING_TWO_PAIR_BYTES, + ), + limb7_yul_coeffs: crate::lowering::quotient_numerator::vm::LIMB7_YUL_COEFFS, + wide_limb7_yul_coeffs: crate::lowering::quotient_numerator::vm::WIDE_LIMB7_YUL_COEFFS, + fr_delta: crate::lowering::quotient_numerator::vm::fr_delta_literal(), + embedded_vk: None, + expected_vk_codehash: Some(U256::from(1u64)), + vk_len: 0, + num_instances: 1, + k: 8, + proof_len, + codegen_layout: super::VerifierCodegenLayout { + proof: proof_layout, + }, + memory, + vk_header: Default::default(), + vk_mptr: Ptr::memory(0x1000), + challenge_mptr: Ptr::memory(0x1200), + theta_mptr: Ptr::memory(0x1300), + proof_cptr: Ptr::calldata(proof_cptr), + abi_selector_bytes: crate::lowering::layout::abi::SELECTOR_BYTES, + abi_proof_head_offset: crate::lowering::layout::abi::VERIFY_PROOF_PROOF_HEAD_OFFSET, + abi_instances_head_cptr: crate::lowering::layout::abi::SELECTOR_BYTES + WORD_BYTES, + num_instance_cptr: proof_cptr + proof_len, + instance_cptr: proof_cptr + proof_len + WORD_BYTES, + quotient_comm_cptr: Ptr::calldata(proof_cptr + non_quotient_g1s * G1_BYTES), + num_neg_lagranges: 0, + user_phases: vec![], + num_user_challenges: 0, + num_lookups, + num_permutation_zs, + lookup_h_plus_acc: lookup_helper_chunks_total + num_lookups, + num_trashcans, + num_quotients, + num_evals, + comms_mptr_base: Ptr::memory(comms_mptr_base), + reversed_evals_mptr: Ptr::memory(0x3000), + selector_acc_mptr, + quotient_external: None, + expected_quotient_len: None, + expected_quotient_codehash: None, + quotient_inline_computations: vec![], + quotient_eval_numer_computations: vec![], + quotient_post_vm_computations: vec![], + quotient_native_permutation_computation: vec![], + quotient_native_lookup_computation: vec![], + quotient_native_identity_computations: vec![], + quotient_program: Some(QuotientProgram { + len: 0, + op_usage: Default::default(), + mem_usage: Default::default(), + const_mptr: 0, + eval_numer_mptr: 0, + trace_id_mptr: 0, + selector_power_mptr: 0, + selector_max_power: 0, + selector_tail_updates: vec![], + stack_mptr: 0, + program_mptr: 0, + }), + pcs_computations: vec![], + simple_selector_cols: vec![], + proof_commit_trace_base: crate::lowering::layout::trace::PROOF_COMMIT_BASE, + proof_eval_trace_base: crate::lowering::layout::trace::PROOF_EVAL_BASE, + quotient_identity_trace_base: crate::lowering::layout::trace::QUOTIENT_IDENTITY_BASE, + selector_trace_base: crate::lowering::layout::trace::SELECTOR_FOLD_BASE, + fixed_comm_mptr: 0, + truncated_challenges: false, + expected_has_accumulator: false, + expected_acc_offset: 0, + expected_num_acc_limbs: 0, + expected_num_acc_limb_bits: 0, + expected_acc_has_carried_scalars: false, + acc_fixed_bases: vec![], + } + } + + #[test] + fn verifying_key_payload_layout_matches_rendered_byte_order() { + let mut vk = synthetic_vk(2, 1); + let header_words = vk.constants.len(); + vk.quotient_const_offset_words = Some(header_words); + vk.quotient_const_words = 2; + vk.quotient_program_offset_words = Some(header_words + 2); + vk.quotient_program_words = 3; + vk.constants.extend((0..2).map(|_| ("quotient_const", U256::ZERO))); + vk.constants.extend((0..3).map(|_| ("quotient_program", U256::ZERO))); + + let layout = vk.payload_layout().unwrap(); + + assert_eq!( + layout.word_offset(PayloadSectionKind::QuotientConstants).unwrap(), + header_words + ); + assert_eq!( + layout.word_offset(PayloadSectionKind::QuotientProgram).unwrap(), + header_words + 2 + ); + assert_eq!( + layout.word_offset(PayloadSectionKind::FixedCommitments).unwrap(), + vk.constants.len() + ); + assert_eq!(layout.total_bytes(), vk.len()); + vk.validate_payload_layout().unwrap(); + } + + #[test] + fn verifying_key_payload_layout_rejects_stale_quotient_offsets() { + let mut vk = synthetic_vk(1, 1); + vk.quotient_const_offset_words = Some(vk.constants.len() + 1); + vk.quotient_const_words = 1; + vk.quotient_program_offset_words = Some(vk.constants.len() + 1); + vk.quotient_program_words = 0; + vk.constants.push(("quotient_const", U256::ZERO)); + + let err = vk.validate_payload_layout().expect_err("stale quotient offset rejected"); + + assert!(err.contains("quotient const offset mismatch")); + } + + #[test] + fn vk_layout_byte_consistency() { + // For a synthetic VK with 31 named scalars + N=2 fixed + M=3 + // permutation commitments, expect: + // len() = 31*32 + (2+3)*4*32 = 31*32 + 20*32 = 51*32 = 1632 bytes + // bytes().len() == len() + let vk = synthetic_vk(2, 3); + let expected_len = 31 * 32 + (2 + 3) * 4 * 32; + assert_eq!(vk.len(), expected_len); + assert_eq!(vk.bytes().len(), expected_len); + + // The first 32 bytes of bytes() should encode `vk_digest`. + let head = &vk.bytes()[..32]; + let mut buf = [0u8; 32]; + buf.copy_from_slice(head); + let head_u256 = U256::from_be_bytes(buf); + assert_eq!(head_u256, U256::from(0xde_u64)); + + // Word index of NEG_S_G2_BASE_MPTR = 23 (vk_mptr + 23). + // Verify the corresponding bytes match the synthetic value 0x30. + let off = 23 * 32; + let mut buf = [0u8; 32]; + buf.copy_from_slice(&vk.bytes()[off..off + 32]); + let neg_s_g2_x_c0_hi = U256::from_be_bytes(buf); + assert_eq!(neg_s_g2_x_c0_hi, U256::from(0x30_u64)); + + // First fixed_comm starts at word 31. + let off = 31 * 32; + let mut buf = [0u8; 32]; + buf.copy_from_slice(&vk.bytes()[off..off + 32]); + assert_eq!(U256::from_be_bytes(buf), U256::from(0x40_u64)); + + // First permutation_comm starts at word 31 + 4*N_FIXED = 39. + let off = 39 * 32; + let mut buf = [0u8; 32]; + buf.copy_from_slice(&vk.bytes()[off..off + 32]); + assert_eq!(U256::from_be_bytes(buf), U256::from(0x80_u64)); + } + + #[test] + fn verifier_layout_validation_checks_calldata_and_memory_cursors() { + let verifier = synthetic_verifier(); + verifier.validate_layout().expect("synthetic layout"); + } + + #[test] + fn verifier_layout_validation_rejects_cursor_drift() { + let mut verifier = synthetic_verifier(); + verifier.num_instance_cptr += 0x20; + let err = verifier.validate_layout().unwrap_err(); + assert!( + err.contains("proof calldata layout mismatch"), + "unexpected layout error: {err}" + ); + } + + #[test] + fn verifier_layout_validation_rejects_vk_challenge_overlap() { + let mut verifier = synthetic_verifier(); + verifier.vk_len = 0x220; + let err = verifier.validate_layout().unwrap_err(); + assert!( + err.contains("VK memory layout mismatch"), + "unexpected layout error: {err}" + ); + } + + #[test] + fn verifier_layout_validation_rejects_pcs_scratch_overflow() { + let mut verifier = synthetic_verifier(); + verifier.memory.pcs.rot_points_words = 29; + let err = verifier.validate_layout().unwrap_err(); + assert!( + err.contains("ROT_POINTS_MPTR needs 29 word"), + "unexpected layout error: {err}" + ); + + let mut verifier = synthetic_verifier(); + verifier.memory.pcs.x1_powers_words = 66; + let err = verifier.validate_layout().unwrap_err(); + assert!( + err.contains("X1_POWERS_MPTR needs 66 word"), + "unexpected layout error: {err}" + ); + + let mut verifier = synthetic_verifier(); + verifier.memory.pcs.q_com_words = 1; + let err = verifier.validate_layout().unwrap_err(); + assert!( + err.contains("Q_COM_MPTR needs 1 word"), + "unexpected layout error: {err}" + ); + + let mut verifier = synthetic_verifier(); + verifier.memory.pcs.q_eval_set_words = 57; + let err = verifier.validate_layout().unwrap_err(); + assert!( + err.contains("Q_EVAL_SET_MPTR needs 57 word"), + "unexpected layout error: {err}" + ); + } + + #[test] + fn vk_renders_and_returns_correct_length() { + let vk = synthetic_vk(2, 3); + let mut s = String::new(); + vk.render(&mut s).expect("VK render"); + // The constructor returns one INVALID byte followed by the payload. The + // verifier pins that full runtime but copies only the payload from + // byte offset 1. Our `hex` filter left-pads odd-length hex literals + // with a leading zero, so 0x661 (3 hex digits) renders as "0x0661". + let runtime = vk.runtime_bytes(); + assert_eq!(runtime.len(), vk.runtime_len()); + assert_eq!(runtime[0], VK_RUNTIME_PREFIX); + assert_eq!(&runtime[1..], vk.bytes().as_slice()); + + let raw_hex = format!("{:x}", vk.runtime_len()); + let padded_hex = if raw_hex.len() % 2 == 1 { + format!("0{raw_hex}") + } else { + raw_hex + }; + let expected_return = format!("return(runtime, 0x{padded_hex})"); + assert!( + s.contains(&expected_return), + "rendered VK missing expected return statement {expected_return} in:\n{s}" + ); + assert!( + s.contains("let runtime := 0x80"), + "VK constructor runtime buffer must start after Solidity's reserved words" + ); + assert!( + s.contains("let payload := add(runtime, 0x01)"), + "VK payload must start after the INVALID runtime prefix" + ); + assert!( + s.contains("mstore8(runtime, 0xfe)"), + "VK runtime must start with an unconditional INVALID opcode" + ); + // It should `mstore` the very first scalar (vk_digest) at payload + 0. + assert!( + s.contains("mstore(add(payload, 0x0000),"), + "vk_digest mstore at payload offset 0" + ); + // The first permutation commitment is at byte offset 0x4e0 + // (39 * 32 = 1248 = 0x4e0). + assert!( + s.contains("mstore(add(payload, 0x04e0),"), + "permutation_comms[0].x_hi at byte offset 0x4e0" + ); + } +} diff --git a/proofs/solidity-verifier/src/lowering/render/yul.rs b/proofs/solidity-verifier/src/lowering/render/yul.rs new file mode 100644 index 000000000..59badb575 --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/render/yul.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Yul formatting helpers live in `lowering::encoding` for now. +//! +//! This module is reserved as the render-side home for those helpers once the +//! larger formatting split is made. diff --git a/proofs/solidity-verifier/src/lowering/tests.rs b/proofs/solidity-verifier/src/lowering/tests.rs new file mode 100644 index 000000000..10f2fcdef --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/tests.rs @@ -0,0 +1,3261 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Lowering regression tests. +//! +//! These tests pin layout constants, template contracts, generated Yul +//! structure, and compact quotient VM invariants so the reorganization remains +//! behavior-preserving. + +use std::collections::HashMap; + +use ff::{Field, PrimeField}; +use midnight_curves::{Bls12, Fq}; +use midnight_proofs::{ + circuit::{Layouter, SimpleFloorPlanner, Value}, + plonk::{ + keygen_vk_with_k, Advice, Circuit, Column, ConstraintSystem, Constraints, + Error as PlonkError, Expression, FirstPhase, Selector, VerifyingKey, + }, + poly::{ + kzg::{params::ParamsKZG, KZGCommitmentScheme}, + Rotation, + }, +}; +use rand::SeedableRng; +use rand_chacha::ChaCha8Rng; +use ruint::aliases::U256; + +use super::{ + config::{ + DEFAULT_HYBRID_QUOTIENT_INLINE_IDENTITIES, DEFAULT_QUOTIENT_LIMB_VM_OPS, + DEFAULT_QUOTIENT_NATIVE_GATES, + }, + encoding::*, + kzg, + layout::{self, WORD_BYTES}, + quotient_numerator::vm::*, + VerifierBuildInputs, +}; +use crate::{ + api::{ + AccumulatorEncoding, CommittedInstanceCommitmentKind, GeneratorConfig, GeneratorError, + QuotientIdentityManifestTarget, QuotientIdentitySource, + }, + SolidityGenerator, +}; + +/// Solidity/Yul template corpus scanned by documentation and regression tests. +const VERIFIER_TEMPLATE_CORPUS: &str = concat!( + include_str!("../../templates/contracts/Halo2Verifier.sol"), + "\n", + include_str!("../../templates/partials/verifier/Constants.sol"), + "\n", + include_str!("../../templates/partials/verifier/PrecompileSmoke.sol"), + "\n", + include_str!("../../templates/partials/verifier/Constructors.sol"), + "\n", + include_str!("../../templates/partials/verifier/AssemblyHelpers.yul"), + "\n", + include_str!("../../templates/partials/verifier/AccumulatorHelpers.yul"), + "\n", + include_str!("../../templates/partials/verifier/TraceAndGasHelpers.yul"), + "\n", + include_str!("../../templates/partials/verifier/VkLoading.yul"), + "\n", + include_str!("../../templates/partials/verifier/TranscriptProofParser.yul"), + "\n", + include_str!("../../templates/partials/verifier/Lagrange.yul"), + "\n", + include_str!("../../templates/partials/verifier/QuotientAndLinearization.yul"), + "\n", + include_str!("../../templates/partials/verifier/Pcs.yul"), + "\n", + include_str!("../../templates/partials/verifier/FinalPairing.yul"), + "\n", + include_str!("../../templates/partials/verifier/TraceReturn.yul"), + "\n", + include_str!("../../templates/partials/quotient_numerator/QuotientHelpers.yul"), + "\n", + include_str!("../../templates/partials/quotient_numerator/QuotientNumeratorBlock.yul"), +); + +/// Production lowering sources scanned for required comment/documentation text. +const LOWERING_SOURCE_CORPUS: &str = concat!( + include_str!("../builder/mod.rs"), + "\n", + include_str!("../builder/api.rs"), + "\n", + include_str!("../builder/render.rs"), + "\n", + include_str!("../builder/repack.rs"), + "\n", + include_str!("artifacts.rs"), + "\n", + include_str!("diagnostics.rs"), + "\n", + include_str!("plan.rs"), + "\n", + include_str!("quotient.rs"), + "\n", + include_str!("vk.rs"), +); + +/// Source corpus used to keep stale module names out of the public crate. +const MODULE_RENAME_SOURCE_CORPUS: &str = concat!( + include_str!("../lib.rs"), + "\n", + include_str!("../api.rs"), + "\n", + include_str!("../evm.rs"), + "\n", + include_str!("../builder/mod.rs"), + "\n", + include_str!("../builder/api.rs"), + "\n", + include_str!("../builder/render.rs"), + "\n", + include_str!("../builder/repack.rs"), + "\n", + include_str!("mod.rs"), + "\n", + include_str!("artifacts.rs"), + "\n", + include_str!("calldata.rs"), + "\n", + include_str!("diagnostics.rs"), + "\n", + include_str!("plan.rs"), + "\n", + include_str!("quotient.rs"), + "\n", + include_str!("vk.rs"), +); + +/// Lowering-only corpus used to enforce no dependency on the builder facade. +const LOWERING_ONLY_SOURCE_CORPUS: &str = concat!( + include_str!("mod.rs"), + "\n", + include_str!("artifacts.rs"), + "\n", + include_str!("calldata.rs"), + "\n", + include_str!("diagnostics.rs"), + "\n", + include_str!("plan.rs"), + "\n", + include_str!("quotient.rs"), + "\n", + include_str!("vk.rs"), +); + +/// Return the concatenated verifier template corpus. +fn verifier_template_corpus() -> &'static str { + VERIFIER_TEMPLATE_CORPUS +} + +/// Return the concatenated production lowering source corpus. +fn lowering_source_corpus() -> &'static str { + LOWERING_SOURCE_CORPUS +} + +#[test] +fn module_rename_boundaries_stay_clean() { + for stale in [ + concat!("crate::", "codegen"), + concat!("crate::", "generator"), + concat!("mod ", "codegen"), + concat!("mod ", "generator"), + ] { + assert!( + !MODULE_RENAME_SOURCE_CORPUS.contains(stale), + "source should not refer to stale module path `{stale}`" + ); + } + assert!( + !LOWERING_ONLY_SOURCE_CORPUS.contains(concat!("crate::", "builder")), + "lowering must not depend on the builder facade" + ); +} + +#[derive(Default)] +struct TestQuotientExpressionEnv { + fixed: HashMap<(usize, i32), QuotientExpr>, + advice: HashMap<(usize, i32), QuotientExpr>, + instance: HashMap<(usize, i32), QuotientExpr>, + challenges: HashMap, +} + +impl QuotientExpressionEnv for TestQuotientExpressionEnv { + /// Test expressions should already have selector handling resolved. + fn selector(&self, _selector: Selector) -> QuotientExpr { + panic!("test expression should not contain selectors") + } + + /// Return a fixed-column expression fixture. + fn fixed(&self, column_index: usize, rotation: i32) -> QuotientExpr { + self.fixed[&(column_index, rotation)].clone() + } + + /// Return an advice-column expression fixture. + fn advice(&self, column_index: usize, rotation: i32) -> QuotientExpr { + self.advice[&(column_index, rotation)].clone() + } + + /// Return an instance-column expression fixture. + fn instance(&self, column_index: usize, rotation: i32) -> QuotientExpr { + self.instance[&(column_index, rotation)].clone() + } + + /// Return a challenge expression fixture. + fn challenge(&self, index: usize) -> QuotientExpr { + self.challenges[&index].clone() + } +} + +#[derive(Clone, Debug)] +struct LoweringPlanTestConfig { + advice: Column, + selector: Selector, +} + +#[derive(Clone, Debug, Default)] +struct LoweringPlanTestCircuit; + +impl Circuit for LoweringPlanTestCircuit { + /// Config columns for the synthetic lowering-plan circuit. + type Config = LoweringPlanTestConfig; + /// Simple floor planner is enough for the one-row fixture. + type FloorPlanner = SimpleFloorPlanner; + /// The fixture circuit has no extra parameters. + type Params = (); + + /// Return an empty witness copy of the fixture. + fn without_witnesses(&self) -> Self { + Self + } + + /// Configure a tiny circuit that still exercises both instance columns. + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + let advice = meta.advice_column(); + let committed_instance = meta.instance_column(); + let public_instance = meta.instance_column(); + let selector = meta.selector(); + + meta.create_gate("lowering plan balance", |meta| { + let advice = meta.query_advice(advice, Rotation::cur()); + let committed = meta.query_instance(committed_instance, Rotation::cur()); + let public = meta.query_instance(public_instance, Rotation::cur()); + Constraints::with_selector( + selector, + vec![( + "lowering plan balance", + advice + committed + public - Expression::Constant(Fq::from(7u64)), + )], + ) + }); + + LoweringPlanTestConfig { advice, selector } + } + + /// Assign the single row consumed by the balance gate. + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), PlonkError> { + layouter.assign_region( + || "lowering plan row", + |mut region| { + config.selector.enable(&mut region, 0)?; + region.assign_advice( + || "advice", + config.advice, + 0, + || Value::known(Fq::from(7u64)), + )?; + Ok(()) + }, + ) + } +} + +/// Generate parameters and VK for lowering-plan integration tests. +fn lowering_plan_test_vk() -> ( + ParamsKZG, + VerifyingKey>, +) { + let mut rng = ChaCha8Rng::seed_from_u64(7); + let params = ParamsKZG::::unsafe_setup(4, &mut rng); + let circuit = LoweringPlanTestCircuit; + let vk = keygen_vk_with_k::, _>(¶ms, &circuit, 4) + .expect("test circuit VK should build"); + (params, vk) +} + +#[test] +fn scalar_le_to_be_word_reverses_exactly_one_word() { + let mut le = [0u8; 32]; + le[0] = 0x01; + le[1] = 0x23; + le[30] = 0xab; + le[31] = 0xcd; + + let be = scalar_le_to_be_word(&le); + assert_eq!(be[0], 0xcd); + assert_eq!(be[1], 0xab); + assert_eq!(be[30], 0x23); + assert_eq!(be[31], 0x01); +} + +#[test] +fn external_quotient_frame_covers_vk_and_eval_memory() { + let frame = + VerifierBuildInputs::quotient_external_frame_from_bounds(0x1000, 0x300, 0x1400, 7, 3); + assert_eq!(frame.frame_base, 0x1000); + assert_eq!(frame.frame_len, 0x4e0); + assert_eq!(frame.output_len, 0xa0); + assert_eq!(frame.magic, QUOTIENT_EXTERNAL_MAGIC); +} + +#[test] +fn external_quotient_output_uses_planned_return_buffer() { + let verifier_template = verifier_template_corpus(); + assert!( + verifier_template.contains("QUOTIENT_RETURN_MPTR"), + "main verifier should name the planned external quotient return buffer" + ); + assert!( + verifier_template.contains("let q_out := QUOTIENT_RETURN_MPTR"), + "external quotient staticcall output must not alias selector accumulators" + ); + assert!( + !verifier_template.contains("let q_out := SELECTOR_ACC_MPTR"), + "selector accumulators model only selector buckets, not the two-word quotient return header" + ); +} + +#[test] +fn lowering_plan_reuses_stable_layout_facts() { + let (params, vk) = lowering_plan_test_vk(); + let generator = SolidityGenerator::new(¶ms, &vk, GeneratorConfig::new(1, 1)); + let inputs = generator.inputs(); + + let plan = inputs.lowering_plan(); + let repeated = inputs.lowering_plan(); + + assert_eq!(plan.vk_mptr, repeated.vk_mptr); + assert_eq!(plan.vk.runtime_bytes(), repeated.vk.runtime_bytes()); + plan.vk + .validate_payload_layout() + .expect("planned VK payload layout should validate"); + plan.memory.validate().expect("planned verifier memory should validate"); + assert_eq!(plan.memory.vk_mptr, plan.vk_mptr); + assert_eq!( + plan.pcs_memory_requirements, + kzg::memory_requirements(&plan.meta, &plan.data) + ); + + let repack = plan.repacked_proof_layout_plan(); + assert_eq!(repack.repacked_len(), plan.proof_layout.proof_len); + assert_eq!(repack.num_evals, plan.proof_layout.evals.item_count); + assert_eq!(repack.num_point_sets, plan.proof_layout.q_evals.item_count); + + assert_eq!(plan.quotient.program.len, plan.quotient.build.bytes.len()); + assert!(plan.quotient.build.consts.len() <= plan.vk.quotient_const_words); + assert_eq!(plan.quotient.program.stack_mptr, plan.quotient.stack_mptr); + assert_eq!( + plan.quotient.program.eval_numer_mptr, + plan.quotient.state_slots.eval_numer_mptr + ); + assert_eq!( + plan.quotient.sorted_simple.len(), + plan.meta.num_simple_selectors + ); + + let verifier = inputs.generate_verifier_from_plan(&plan, false, false, false, false, None); + assert_eq!(verifier.codegen_layout.proof, plan.proof_layout); + assert_eq!(verifier.memory.vk_mptr, plan.vk_mptr); + assert_eq!(verifier.proof_len, plan.proof_layout.proof_len); + assert_eq!(verifier.simple_selector_cols, plan.quotient.sorted_simple); +} + +#[test] +fn structured_selector_runs_do_not_group_trace_identities() { + let lowering_source = lowering_source_corpus(); + assert!( + !lowering_source.contains("selector_run_quotient_block") + && !lowering_source.contains("flush_structured_selector_run"), + "selector-run grouping should stay removed from the fixed compact default" + ); + assert!( + lowering_source.contains("fn push_quotient_trace(") + && lowering_source.contains("trace_u256(mload({:#x}), {value})") + && lowering_source.contains("mstore({:#x}, add(mload({:#x}), 1))"), + "trace builds must still emit one trace event per emitted identity" + ); +} + +#[test] +fn compact_quotient_default_matches_gas_capped_setting() { + assert_eq!(DEFAULT_HYBRID_QUOTIENT_INLINE_IDENTITIES, 4); + assert_eq!(DEFAULT_QUOTIENT_NATIVE_GATES, 4); + let limb_vm_ops = DEFAULT_QUOTIENT_LIMB_VM_OPS; + assert!(limb_vm_ops); + + let docs = include_str!("../../docs/reference/QUOTIENT_NUMERATOR_EVALUATOR.md"); + assert!( + docs.contains("direct inline identities: 4"), + "quotient evaluator docs should record the gas-capped compact default" + ); + assert!( + docs.contains("structured trash suffix: on"), + "quotient evaluator docs should record the gas-capped structured-tail default" + ); + assert!( + docs.contains("limb VM ops: on"), + "quotient evaluator docs should record the limb-op default" + ); + assert!( + docs.contains("VM CSE: off"), + "quotient evaluator docs should record that temp-backed VM CSE is disabled" + ); +} + +#[test] +fn quotient_forward_y_batch_matches_rust_reverse_fold() { + let y = Fq::from(17u64); + let evals = [ + Fq::from(3u64), + Fq::from(5u64), + Fq::from(7u64), + Fq::from(11u64), + Fq::from(13u64), + ]; + + let solidity_forward = evals.iter().fold(Fq::ZERO, |acc, eval| acc * y + eval); + + let mut rust_reverse = Fq::ZERO; + let mut y_pow = Fq::ONE; + for eval in evals.iter().rev() { + rust_reverse += *eval * y_pow; + y_pow *= y; + } + + assert_eq!(solidity_forward, rust_reverse); +} + +#[test] +fn quotient_selector_gap_fold_matches_rust_reverse_fold() { + let y = Fq::from(19u64); + let evals = [ + Fq::from(2u64), + Fq::from(0u64), + Fq::from(23u64), + Fq::from(29u64), + ]; + let selector_positions = [true, false, true, true]; + + let mut y_powers = vec![Fq::ONE; evals.len()]; + for idx in 1..y_powers.len() { + y_powers[idx] = y_powers[idx - 1] * y; + } + + let mut previous = None; + let mut selector_acc = Fq::ZERO; + for (idx, (eval, selected)) in evals.iter().zip(selector_positions).enumerate() { + if selected { + let gap = previous.map_or(0, |prev| idx - prev); + selector_acc *= y_powers[gap]; + selector_acc += *eval; + previous = Some(idx); + } + } + let tail = evals.len() - 1 - previous.expect("selector identity present"); + let solidity_selector_acc = selector_acc * y_powers[tail]; + + let mut rust_selector_acc = Fq::ZERO; + let mut y_pow = Fq::ONE; + for (eval, selected) in evals.iter().zip(selector_positions).rev() { + if selected { + rust_selector_acc += *eval * y_pow; + } + y_pow *= y; + } + + assert_eq!(solidity_selector_acc, rust_selector_acc); +} + +#[test] +fn quotient_program_artifacts_match_typed_identity_stream() { + let y = Fq::from(23u64); + let mut values = HashMap::new(); + for (ptr, value) in [ + (0x100, 3), + (0x120, 5), + (0x140, 7), + (0x160, 11), + (0x180, 13), + (0x1a0, 17), + (0x1c0, 19), + ] { + values.insert(ptr, Fq::from(value)); + } + + let mem = |ptr| QuotientExpr::Mem(QuotientMem::Literal(ptr)); + let identities = vec![ + test_quotient_identity_with_expr( + 0, + QuotientTarget::Main, + quotient_add_expr(mem(0x100), mem(0x120)), + ), + test_quotient_identity_with_expr( + 1, + QuotientTarget::Selector(0), + quotient_mul_expr(mem(0x140), mem(0x160)), + ), + test_quotient_identity_with_expr(2, QuotientTarget::Selector(1), { + QuotientExpr::Neg(Box::new(mem(0x180))) + }), + test_quotient_identity_with_expr( + 3, + QuotientTarget::Main, + quotient_mul_expr(quotient_add_expr(mem(0x100), mem(0x120)), mem(0x1a0)), + ), + test_quotient_identity_with_expr( + 4, + QuotientTarget::Selector(0), + quotient_add_expr(mem(0x1c0), QuotientExpr::Const(U256::from(29u64))), + ), + ]; + + let selector_fold = VerifierBuildInputs::selector_fold_plan(&identities, 2); + assert_eq!( + selector_fold.gaps_by_identity, + vec![None, Some(0), Some(0), None, Some(3)] + ); + assert_eq!(selector_fold.tail_exponents, vec![0, 2]); + + let mut builder = QuotientProgramBuilder::default(); + for identity in &identities { + builder.identity_expr( + &identity.expr, + identity.target, + selector_fold.gap_for(identity), + ); + } + validate_quotient_program(&builder.bytes).expect("synthetic quotient program should validate"); + + let expected = expected_linearization_artifacts_for_test(&identities, &values, y, 2); + let actual = eval_quotient_program_artifacts_for_test( + &builder.bytes, + &builder.consts, + &values, + y, + &selector_fold.tail_exponents, + ); + + assert_eq!(actual, expected); +} + +#[test] +fn quotient_execution_manifest_expands_native_callback_ranges() { + let inline = test_quotient_identity_with_expr( + 0, + QuotientTarget::Main, + QuotientExpr::Const(U256::from(3u64)), + ); + let native_gate = test_quotient_identity_with_expr( + 1, + QuotientTarget::Selector(0), + QuotientExpr::Const(U256::from(5u64)), + ); + let interpreted_gate = test_quotient_identity_with_expr( + 2, + QuotientTarget::Main, + QuotientExpr::Const(U256::from(7u64)), + ); + let permutation_0 = test_quotient_identity( + 3, + QuotientIdentitySource::Permutation { identity_index: 0 }, + QuotientTarget::Main, + ); + let permutation_1 = test_quotient_identity( + 4, + QuotientIdentitySource::Permutation { identity_index: 1 }, + QuotientTarget::Main, + ); + let lookup = test_quotient_identity( + 5, + QuotientIdentitySource::Lookup { + identity_index: 0, + lookup_index: 0, + lookup_name: "lookup_0".to_string(), + }, + QuotientTarget::Main, + ); + let trash = test_quotient_identity( + 6, + QuotientIdentitySource::Trash { + trash_index: 0, + trash_name: "trash_0".to_string(), + }, + QuotientTarget::Main, + ); + let expected = vec![ + inline.clone(), + native_gate.clone(), + interpreted_gate.clone(), + permutation_0.clone(), + permutation_1.clone(), + lookup.clone(), + trash.clone(), + ]; + let plan = test_quotient_execution_plan( + expected.clone(), + vec![inline], + vec![ + QuotientProgramItem::NativeIdentity(0), + QuotientProgramItem::Identity(interpreted_gate), + QuotientProgramItem::NativePermutation, + QuotientProgramItem::NativeLookup, + ], + vec![native_gate], + vec![permutation_0, permutation_1], + vec![lookup], + vec![trash], + ); + + plan.validate_execution_manifest(&expected) + .expect("mixed execution plan should preserve identity stream"); + let executions = plan + .execution_manifest() + .expect("manifest should reconstruct") + .into_iter() + .map(|entry| entry.execution) + .collect::>(); + + assert_eq!( + executions, + vec![ + QuotientExecutionKind::Inline, + QuotientExecutionKind::NativeIdentity { native_index: 0 }, + QuotientExecutionKind::Interpreted, + QuotientExecutionKind::NativePermutation, + QuotientExecutionKind::NativePermutation, + QuotientExecutionKind::NativeLookup, + QuotientExecutionKind::StructuredTail, + ] + ); +} + +#[test] +fn quotient_execution_manifest_rejects_reordered_native_range() { + let inline = test_quotient_identity_with_expr( + 0, + QuotientTarget::Main, + QuotientExpr::Const(U256::from(3u64)), + ); + let permutation_0 = test_quotient_identity( + 1, + QuotientIdentitySource::Permutation { identity_index: 0 }, + QuotientTarget::Main, + ); + let permutation_1 = test_quotient_identity( + 2, + QuotientIdentitySource::Permutation { identity_index: 1 }, + QuotientTarget::Main, + ); + let expected = vec![inline.clone(), permutation_0.clone(), permutation_1.clone()]; + let mut plan = test_quotient_execution_plan( + expected.clone(), + vec![inline], + vec![QuotientProgramItem::NativePermutation], + Vec::new(), + vec![permutation_0, permutation_1], + Vec::new(), + Vec::new(), + ); + plan.native_permutation_identities.swap(0, 1); + + let err = plan + .validate_execution_manifest(&expected) + .expect_err("reordered native range must not validate"); + assert!( + err.contains("mismatch at position"), + "unexpected manifest error: {err}" + ); +} + +#[test] +fn quotient_execution_manifest_rejects_bad_native_identity_index() { + let native_gate = test_quotient_identity_with_expr( + 0, + QuotientTarget::Main, + QuotientExpr::Const(U256::from(5u64)), + ); + let plan = test_quotient_execution_plan( + vec![native_gate.clone()], + Vec::new(), + vec![QuotientProgramItem::NativeIdentity(1)], + vec![native_gate], + Vec::new(), + Vec::new(), + Vec::new(), + ); + + let err = plan + .execution_manifest() + .expect_err("out-of-range native callback body must fail"); + assert!( + err.contains("has no callback body"), + "unexpected native identity error: {err}" + ); +} + +#[test] +fn quotient_program_items_emit_bytecode_markers_in_manifest_order() { + let interpreted = test_quotient_identity_with_expr( + 0, + QuotientTarget::Selector(0), + QuotientExpr::Const(U256::from(11u64)), + ); + let selector_fold = + VerifierBuildInputs::selector_fold_plan(std::slice::from_ref(&interpreted), 1); + let mut builder = QuotientProgramBuilder::default(); + + builder.native_identity(7); + builder.identity_expr( + &interpreted.expr, + interpreted.target, + selector_fold.gap_for(&interpreted), + ); + builder.native_permutation(); + builder.native_lookup(); + + validate_quotient_program(&builder.bytes) + .expect("native markers and interpreted fold should validate"); + assert_eq!( + quotient_bytecode_events_for_test(&builder.bytes), + vec![ + TestQuotientBytecodeEvent::NativeIdentity(7), + TestQuotientBytecodeEvent::Fold(QuotientTarget::Selector(0)), + TestQuotientBytecodeEvent::NativePermutation, + TestQuotientBytecodeEvent::NativeLookup, + ] + ); +} + +#[test] +fn selector_sparse_fold_exhaustive_matches_naive_global_y_powers() { + let y = Fq::from(31u64); + for len in 1..=8usize { + let cases = 3usize.pow(len as u32); + for mut case in 0..cases { + let mut identities = Vec::with_capacity(len); + for idx in 0..len { + let digit = case % 3; + case /= 3; + let target = match digit { + 0 => QuotientTarget::Main, + 1 => QuotientTarget::Selector(0), + _ => QuotientTarget::Selector(1), + }; + identities.push(test_quotient_identity_with_expr( + idx, + target, + QuotientExpr::Const(U256::from((idx as u64 + 2) * 17)), + )); + } + + let selector_fold = VerifierBuildInputs::selector_fold_plan(&identities, 2); + let values = HashMap::new(); + let expected = expected_linearization_artifacts_for_test(&identities, &values, y, 2); + + let mut sparse_main = Fq::ZERO; + let mut sparse_selectors = vec![Fq::ZERO; 2]; + for identity in &identities { + let value = eval_quotient_expr_for_test(&identity.expr, &values); + match identity.target { + QuotientTarget::Main => { + sparse_main = sparse_main * y + value; + } + QuotientTarget::Selector(selector_idx) => { + sparse_main *= y; + let gap = + selector_fold.gap_for(identity).expect("selector identity has gap"); + sparse_selectors[selector_idx] = + sparse_selectors[selector_idx] * fq_pow(y, gap) + value; + } + } + } + for (bucket, tail) in + sparse_selectors.iter_mut().zip(selector_fold.tail_exponents.iter()) + { + *bucket *= fq_pow(y, *tail); + } + + assert_eq!(sparse_main, expected.main_numerator); + assert_eq!(sparse_selectors, expected.selector_buckets); + assert_eq!(-sparse_main, expected.quotient_expected_eval); + } + } +} + +#[test] +fn quotient_identity_manifest_preserves_order_and_metadata() { + let gate_source = QuotientIdentitySource::Gate { + gate_index: 2, + gate_name: "custom_gate".to_string(), + constraint_index: 1, + constraint_name: "constraint_b".to_string(), + polynomial_index: 1, + }; + let parts = QuotientIdentityParts { + gates: vec![test_quotient_identity( + 0, + gate_source.clone(), + QuotientTarget::Selector(0), + )], + permutation: vec![test_quotient_identity( + 1, + QuotientIdentitySource::Permutation { identity_index: 0 }, + QuotientTarget::Main, + )], + lookup: vec![test_quotient_identity( + 2, + QuotientIdentitySource::Lookup { + identity_index: 0, + lookup_index: 0, + lookup_name: "lookup_0".to_string(), + }, + QuotientTarget::Main, + )], + trash: vec![test_quotient_identity( + 3, + QuotientIdentitySource::Trash { + trash_index: 0, + trash_name: "partial_round_gate".to_string(), + }, + QuotientTarget::Main, + )], + sorted_simple: vec![42], + }; + + let manifest = parts.manifest(); + + assert_eq!(manifest.gate_identities, 1); + assert_eq!(manifest.permutation_identities, 1); + assert_eq!(manifest.lookup_identities, 1); + assert_eq!(manifest.trash_identities, 1); + assert_eq!( + manifest.entries.iter().map(|entry| entry.global_index).collect::>(), + vec![0, 1, 2, 3] + ); + assert_eq!(manifest.entries[0].source, gate_source); + assert_eq!( + manifest.entries[0].target, + QuotientIdentityManifestTarget::Selector { + selector_index: 0, + fixed_column: 42, + } + ); + assert!(matches!( + manifest.entries[3].source, + QuotientIdentitySource::Trash { ref trash_name, .. } if trash_name == "partial_round_gate" + )); +} + +#[test] +fn limb7_linear_chain_is_recognized_as_native_helper_call() { + let lines = [ + "let c0 := 0x100000000000000", + "let m0 := mulmod(c0, x1, r)", + "let a0 := addmod(x0, m0, r)", + "let m1 := mulmod(0x10000000000000000000000000000, x2, r)", + "let a1 := addmod(a0, m1, r)", + "let m2 := mulmod(0x400000000, x3, r)", + "let a2 := addmod(a1, m2, r)", + "let m3 := mulmod(0x40000000000000000000000, x4, r)", + "let a3 := addmod(a2, m3, r)", + "let m4 := mulmod(0x1000, x5, r)", + "let a4 := addmod(a3, m4, r)", + "let m5 := mulmod(0x100000000000000000, x6, r)", + "let a5 := addmod(a4, m5, r)", + ] + .iter() + .map(|line| line.to_string()) + .collect::>(); + + let specialized = VerifierBuildInputs::specialize_limb7_chains(&lines); + + assert_eq!( + specialized, + vec![ + "let c0 := 0x100000000000000".to_string(), + "let a5 := q_limb7(x0, x1, x2, x3, x4, x5, x6)".to_string(), + ] + ); +} + +#[test] +fn transcript_memory_bound_handles_wide_bls_advice_phase() { + let mut cs = ConstraintSystem::default(); + let mut advices = Vec::new(); + for _ in 0..64 { + advices.push(cs.advice_column()); + } + cs.create_gate("open wide advice phase", |meta| { + let acc = advices.iter().fold(Expression::Constant(Fq::ZERO), |acc, col| { + acc + meta.query_advice(*col, Rotation::cur()) + }); + Constraints::without_selector(vec![("open wide advice phase", acc)]) + }); + let meta = ConstraintSystemMeta::new(&cs, 0); + + let words = VerifierBuildInputs::transcript_buffer_words_bound(&meta, 0); + + // Regression for the BN254-era `n * 2 + 1` sizing bug: the BLS + // transcript absorbs each proof commitment as a 128-byte + // EIP-2537-padded G1, so 64 first-phase advice commitments need + // vk_digest + committed_pi + num_instances + 64 G1s + squeeze seed. + let first_phase_run = 32 + 128 + 32 + 64 * 128 + 32; + assert!(words * 0x20 >= first_phase_run); + assert!( + words > 64 * 2 + 1, + "transcript memory bound must not regress to the BN254 stride" + ); +} + +#[test] +fn eip2537_calls_use_bounded_gas_literals() { + let verifier_template = verifier_template_corpus(); + let pcs_codegen = include_str!("kzg/mod.rs"); + + for source in [verifier_template, pcs_codegen] { + assert!( + !source.contains("staticcall(gas(), 0x0b"), + "G1ADD calls must use bounded gas caps" + ); + assert!( + !source.contains("staticcall(gas(), 0x0c"), + "G1MSM calls must use bounded gas caps" + ); + assert!( + !source.contains("staticcall(gas(), 0x0f"), + "pairing calls must use bounded gas caps" + ); + } + + assert!(!verifier_template.contains("function g1add_gas_cap()")); + assert!(!verifier_template.contains("function g1msm_gas_cap(input_len)")); + assert!(!verifier_template.contains("function pairing_gas_cap(input_len)")); + assert!( + verifier_template.contains("{{ g1msm_single_gas_cap }}") + && verifier_template.contains("{{ final_pairing_gas_cap }}"), + "main verifier template should render generated gas-cap literals" + ); + assert!( + pcs_codegen.contains("layout::precompile::g1msm_gas_cap") + && pcs_codegen.contains("layout::precompile::G1ADD_GAS_CAP"), + "PCS emitter should compute static EIP-2537 caps at codegen time" + ); +} + +#[test] +fn failed_success_paths_do_not_enter_ec_precompiles() { + let verifier_template = verifier_template_corpus(); + let pcs_codegen = include_str!("kzg/mod.rs"); + + assert!( + verifier_template.contains("if iszero(success) { revert(0, 0) }\n }\n\n {%- if self.expected_has_accumulator %}\n // Fail malformed accumulator public inputs before transcript"), + "ABI/proof length/instance shape checks should fail before accumulator or transcript parsing" + ); + assert!( + verifier_template.contains("success := validate_public_accumulator(success, r)\n if iszero(success) { revert(0, 0) }\n {%- endif %}\n\n {%- if self.gas_checkpoints %}\n gas_checkpoint(2)"), + "accumulator precheck should fail before transcript parsing" + ); + assert!( + verifier_template.contains( + "if iszero(success) { revert(0, 0) }\n\n {%- match quotient_external %}" + ), + "failed Lagrange/common-polynomial setup should fail before quotient reconstruction" + ); + for source in [verifier_template, pcs_codegen] { + assert!( + !source.contains("and(success, staticcall"), + "Yul does not short-circuit and(success, staticcall(...)); guard precompile calls with if success" + ); + assert!( + !source.contains("and(\n success,\n staticcall"), + "multi-line and(success, staticcall(...)) must not reappear" + ); + } + assert!( + verifier_template.contains( + "if iszero(success) { revert(0, 0) }\n success := ec_pairing(success, PAIRING_RHS_MPTR, PAIRING_LHS_MPTR)" + ), + "final pairing block should revert before staging/calling the pairing precompile when success is already false" + ); + assert!( + pcs_codegen.contains("if success {") + && pcs_codegen.contains("success := staticcall({final_msm_gas_cap}") + && pcs_codegen.contains("success := staticcall({}, 0x0b"), + "PCS emitter should guard final MSM/add precompile calls with if success" + ); +} + +#[test] +fn verifier_constructor_smoke_tests_eip2537_precompiles() { + let verifier_template = verifier_template_corpus(); + + assert!( + verifier_template.contains("function require_eip2537_precompiles() private view"), + "generated verifier should include a deployment-time EIP-2537 smoke test" + ); + for required in [ + "G1ADD(identity, identity) -> identity", + "G1MSM([(identity, 0)]) -> identity", + "PAIRING_CHECK([(identity_g1, identity_g2)]) -> true", + "template_constants.eip2537.g1add_gas_cap", + "template_constants.eip2537.g1msm_smoke_gas_cap", + "template_constants.eip2537.pairing_smoke_gas_cap", + "template_constants.eip2537.g1add_address", + "template_constants.eip2537.g1msm_address", + "template_constants.eip2537.pairing_address", + "eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})", + "eq(returndatasize(), {{ template_constants.word_bytes|hex() }})", + ] { + assert!( + verifier_template.contains(required), + "constructor precompile smoke test missing expected check: {required}" + ); + } + assert_eq!( + verifier_template.matches("require_eip2537_precompiles();").count(), + 4, + "every generated constructor shape should run the precompile smoke test" + ); + assert!( + verifier_template.contains("support MCOPY and EIP-2537"), + "generated source should state the target-chain opcode/precompile requirement" + ); +} + +#[test] +fn external_quotient_template_has_no_unpinned_fallback() { + let verifier_template = verifier_template_corpus(); + + assert!( + !verifier_template.contains("AUTHORIZED_QUOTIENT_CODEHASH"), + "external quotient builds must use generated expected length/hash constants" + ); + assert!( + !verifier_template.contains("authorizedQuotient.code.length != 0"), + "constructor must not pin whichever quotient address the deployer passed" + ); +} + +#[test] +fn pinned_dependencies_are_rechecked_during_verification() { + let verifier_template = verifier_template_corpus(); + + for required in [ + "EXPECTED_VK_PAYLOAD_LENGTH", + "eq(extcodesize(vk), EXPECTED_VK_LENGTH)", + "eq(extcodehash(vk), EXPECTED_VK_CODEHASH_WORD)", + "extcodecopy(vk, VK_MPTR, 0x01, EXPECTED_VK_PAYLOAD_LENGTH)", + "eq(extcodesize(quotientEvaluator), EXPECTED_QUOTIENT_LENGTH)", + "eq(extcodehash(quotientEvaluator), EXPECTED_QUOTIENT_CODEHASH_WORD)", + "Re-check the pinned VK dependency on every proof", + "Re-check the pinned runtime before every", + ] { + assert!( + verifier_template.contains(required), + "template should re-check pinned dependency at verification time: {required}" + ); + } +} + +#[test] +fn separate_vk_runtime_is_prefixed_with_invalid_opcode() { + let verifier_template = verifier_template_corpus(); + let vk_template = include_str!("../../templates/contracts/Halo2VerifyingKey.sol"); + + assert!( + vk_template.contains("mstore8(runtime, 0xfe)"), + "VK runtime must start with an unconditional INVALID opcode" + ); + assert!( + vk_template.contains("return(runtime,"), + "VK constructor must return the prefixed runtime, not just the payload" + ); + assert!( + verifier_template.contains("EXPECTED_VK_LENGTH = {{ vk_len + 1 }}"), + "verifier must pin the full INVALID-prefixed VK runtime length" + ); + assert!( + verifier_template.contains("extcodecopy(vk, VK_MPTR, 0x01, EXPECTED_VK_PAYLOAD_LENGTH)"), + "verifier must skip the INVALID prefix while loading the VK payload" + ); +} + +#[test] +fn accumulator_schema_is_checked_against_instance_count() { + let verifier_template = verifier_template_corpus(); + let spec = include_str!("../../docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md"); + + assert!( + verifier_template.contains("eq({{ num_instances }}, calldataload(NUM_INSTANCE_CPTR))"), + "verifier must check calldata instance length against the generated public-input width" + ); + assert!( + verifier_template.contains("add(INSTANCE_CPTR, {{ (num_instances * 32)|hex() }})"), + "verifier must reject extra trailing calldata after the generated instance vector" + ); + assert!( + verifier_template.contains("RHS layout for this generated verifier is fully collapsed"), + "no-tail accumulator renders must explicitly document that no fixed-base scalar tail exists" + ); + assert!( + verifier_template.contains("collapsed point pair"), + "point-pair accumulator renders must explicitly document implicit scalar semantics" + ); + assert!( + verifier_template.contains("RHS layout for this generated verifier is partially"), + "tail accumulator renders must explicitly document fixed-base scalar tail semantics" + ); + assert!( + verifier_template.contains("{%- if acc_fixed_bases.len() > 0 %}"), + "fixed-base scalar tail parsing should only render when generated bases exist" + ); + for required in [ + "GeneratorConfig::new", + "with_accumulator", + "The accumulator is not passed through a separate ABI argument", + "checked tail convention", + ] { + assert!( + spec.contains(required), + "accumulator activation/layout docs should mention: {required}" + ); + } +} + +#[test] +fn accumulator_vk_header_is_specialized_at_codegen() { + let verifier_template = verifier_template_corpus(); + + for expected_check in [ + "eq(mload(HAS_ACCUMULATOR_MPTR)", + "eq(mload(ACC_OFFSET_MPTR)", + "eq(mload(NUM_ACC_LIMBS_MPTR)", + "eq(mload(NUM_ACC_LIMB_BITS_MPTR)", + ] { + assert!( + !verifier_template.contains(expected_check), + "pinned verifier should not reread VK accumulator metadata at runtime: {expected_check}" + ); + } + assert!( + verifier_template.contains("schema\n // values such as instance count and accumulator layout are\n // rendered as constants"), + "template should document why VK schema values are generated constants" + ); + assert!( + verifier_template.contains("{%- if self.expected_has_accumulator %}") + && verifier_template.contains("let bits := {{ self.expected_num_acc_limb_bits }}") + && verifier_template.contains("let n := {{ self.expected_num_acc_limbs }}"), + "accumulator decoding should be rendered only for generated accumulator VKs" + ); +} + +#[test] +fn accumulator_limb_packing_is_checked_before_decoding() { + let verifier_template = verifier_template_corpus(); + + assert!( + verifier_template.contains("function check_acc_coord_packing"), + "accumulator decoder must reject unused high bits in packed limb words" + ); + assert!( + verifier_template.contains("ok := check_acc_coord_packing(src, bits, n, limbs_per_word)"), + "accumulator coordinate decoding must apply packing canonicality before masking limbs" + ); +} + +#[test] +fn accumulator_points_are_prevalidated_before_transcript_work() { + let verifier_template = verifier_template_corpus(); + + for required in [ + "function validate_public_accumulator(success, r) -> out", + "Fail malformed accumulator public inputs before transcript", + "success := validate_public_accumulator(success, r)", + "gas_checkpoint(2) // after VK loading + accumulator public-input precheck", + "Batch the prevalidated public IVC accumulator pairing equation", + ] { + assert!( + verifier_template.contains(required), + "accumulator validation should be split into early precheck and late pairing batch: {required}" + ); + } +} + +#[test] +fn accumulator_decoder_rejects_noncanonical_infinity() { + let verifier_template = verifier_template_corpus(); + + for required in [ + "EIP-2537 reserves affine (0,0) for the point", + "let decoded_zero := iszero(or(or(x_hi, x_lo), or(y_hi, y_lo)))", + "ok := and(ok, iszero(decoded_zero))", + "is_acc_encoded_identity(src)", + ] { + assert!( + verifier_template.contains(required), + "accumulator decoder should reject non-canonical infinity encodings: {required}" + ); + } +} + +#[test] +fn accumulator_encoding_validation_rejects_dead_configs() { + let fully_collapsed = AccumulatorEncoding::new(4, 7, 56); + assert_eq!( + fully_collapsed.fully_collapsed_public_input_words(), + Ok(AccumulatorEncoding::FULLY_COLLAPSED_PUBLIC_INPUT_WORDS) + ); + assert!(fully_collapsed.validate_for_num_instances(14).is_ok()); + assert_eq!(fully_collapsed.fixed_scalar_count(14), Ok(0)); + assert_eq!(fully_collapsed.fixed_scalar_count(17), Ok(3)); + + let point_pair = AccumulatorEncoding::point_pair(4, 7, 56); + assert_eq!( + point_pair.fully_collapsed_public_input_words(), + Ok(AccumulatorEncoding::POINT_PAIR_PUBLIC_INPUT_WORDS) + ); + assert!(point_pair.validate_for_num_instances(12).is_ok()); + assert_eq!(point_pair.fixed_scalar_count(12), Ok(0)); + assert!(matches!( + point_pair.fixed_scalar_count(13), + Err(GeneratorError::UnsupportedAccumulatorEncoding { + reason: "point-pair accumulator encoding does not support a fixed-base scalar tail", + .. + }) + )); + + let wrong_limb_shape = AccumulatorEncoding::new(4, 8, 32); + assert!(matches!( + wrong_limb_shape.validate_for_num_instances(14), + Err(GeneratorError::UnsupportedAccumulatorEncoding { + num_limbs: 8, + num_limb_bits: 32, + .. + }) + )); + + let out_of_bounds = AccumulatorEncoding::new(5, 7, 56); + assert!(matches!( + out_of_bounds.validate_for_num_instances(14), + Err(GeneratorError::UnsupportedAccumulatorEncoding { + offset: 5, + reason: "accumulator public-input tail exceeds num_instances", + .. + }) + )); +} + +#[test] +fn generated_solidity_pragmas_require_mcopy_capable_compiler() { + for (name, source) in [ + ("Halo2Verifier.sol", verifier_template_corpus()), + ( + "Halo2VerifyingKey.sol", + include_str!("../../templates/contracts/Halo2VerifyingKey.sol"), + ), + ( + "Halo2QuotientEvaluator.sol", + include_str!("../../templates/contracts/Halo2QuotientEvaluator.sol"), + ), + ] { + assert!( + source.contains("pragma solidity ^0.8.24;"), + "{name} must require Solidity 0.8.24+ for Cancun Yul opcodes" + ); + } +} + +#[test] +fn verifier_checks_canonical_dynamic_abi_heads() { + let verifier_template = verifier_template_corpus(); + + assert!( + verifier_template.contains( + "eq(calldataload({{ abi_selector_bytes|hex() }}), {{ abi_proof_head_offset|hex() }})" + ), + "verifier must read the proof dynamic ABI head" + ); + assert!( + verifier_template.contains( + "eq(calldataload({{ abi_instances_head_cptr|hex() }}), sub(NUM_INSTANCE_CPTR, {{ abi_selector_bytes|hex() }}))" + ), + "verifier must read the instances dynamic ABI head" + ); +} + +#[test] +fn generated_comments_describe_padded_g1_calldata() { + let verifier_template = verifier_template_corpus(); + + for stale in [ + "zcash-compressed form", + "decompressed inline", + "Helpers: modexp, decompress", + "Each compressed G1 absorbed", + "Per-category bases for decompressed G1 commitments", + ] { + assert!( + !verifier_template.contains(stale), + "generated verifier comments must not describe the old compressed-G1 calldata path: {stale}" + ); + } + + assert!( + verifier_template.contains("proof shim repacks midnight-proofs' native compressed stream"), + "generated verifier comments should document the off-chain repacking boundary" + ); + assert!( + verifier_template.contains("validates and absorbs that 128-byte form"), + "generated verifier comments should describe the current EIP-2537-padded G1 path" + ); +} + +#[test] +fn point_validation_boundary_is_documented_and_plan_checked() { + let verifier_template = verifier_template_corpus(); + let protocol_source = include_str!("protocol/mod.rs"); + let test_source = include_str!("../test.rs"); + + assert!( + verifier_template.contains("This helper does not run an independent curve/subgroup"), + "common_uncompressed_g1 should document that curve/subgroup checks are delegated" + ); + assert!( + verifier_template.contains("consumed by an EIP-2537 G1MSM or pairing path"), + "generated comments should name the subgroup-checking precompile paths" + ); + assert!( + protocol_source.contains("Every proof G1 commitment absorbed into Fiat-Shamir"), + "ProtocolPlan validation should document absorbed-point PCS/precompile coverage" + ); + assert!( + protocol_source.contains("absorbed but never opened by PCS"), + "ProtocolPlan validation should reject absorbed unopened advice commitments" + ); + for required_test in [ + "every_proof_g1_rejects_noncanonical_coordinates", + "every_proof_g1_rejects_off_curve_coordinates", + ] { + assert!( + test_source.contains(required_test), + "negative proof-G1 mutation coverage should include {required_test}" + ); + } +} + +#[test] +fn trace_u256_uses_planned_memory_slot() { + let verifier_template = verifier_template_corpus(); + let quotient_template = include_str!("../../templates/contracts/Halo2QuotientEvaluator.sol"); + let memory_source = include_str!("layout/memory.rs"); + + for source in [verifier_template, quotient_template] { + assert!( + source.contains("TRACE_U256_MPTR"), + "trace_u256 should write through a planned memory slot" + ); + assert!( + !source.contains("0x5e00"), + "trace_u256 must not use the old hard-coded scratch word" + ); + } + assert!( + memory_source.contains("trace_u256_log_word"), + "memory planner should register the trace_u256 log buffer" + ); +} + +#[test] +fn differential_trace_hooks_cover_expected_categories() { + let verifier_template = verifier_template_corpus(); + let pcs_source = include_str!("kzg/mod.rs"); + + for (name, needle) in [ + ( + "user transcript challenges", + "1000 + phase.challenge_offset + j", + ), + ("quotient numerator", "trace_u256(36,"), + ("f_eval", "trace_u256(31,"), + ("final MSM commitment", "trace_point(33,"), + ("pairing lhs input", "trace_point(27,"), + ("pairing rhs input", "trace_point(28,"), + ("final result", "trace_u256(35,"), + ("selector folds", "selector_trace_base + loop.index0"), + ] { + assert!( + verifier_template.contains(needle), + "verifier template missing {name} trace hook" + ); + } + + for (name, needle) in [ + ( + "serialized PCS point sets", + "trace::PCS_SERIALIZED_POINT_SET_BASE + set_idx as u64", + ), + ("PCS q_com commitments", "40000 + set_idx"), + ] { + assert!( + pcs_source.contains(needle), + "PCS emitter missing {name} trace hook" + ); + } +} + +#[test] +fn quotient_vm_opcode_and_token_tables_match_template_cases() { + let quotient_template = + include_str!("../../templates/partials/quotient_numerator/QuotientNumeratorBlock.yul"); + + for spec in QUOTIENT_VM_SPEC.opcodes { + let name = spec.name; + let needle = [ + "case {{ template_constants.quotient_vm.op.", + name, + "|hex() }}", + ] + .concat(); + assert!( + quotient_template.contains(&needle), + "quotient VM template missing opcode {name} template constant" + ); + } + assert!( + !quotient_template.contains("case 0x1a"), + "stale native-trash opcode must not remain in quotient VM template" + ); + + for spec in QUOTIENT_VM_SPEC.mem_tokens { + let name = spec.name; + let field = match name { + "L_0_MPTR" => "l0", + "L_LAST_MPTR" => "l_last", + "L_BLIND_MPTR" => "l_blind", + "BETA_MPTR" => "beta", + "GAMMA_MPTR" => "gamma", + "X_MPTR" => "x", + "THETA_MPTR" => "theta", + "TRASH_CHALLENGE_MPTR" => "trash_challenge", + "INSTANCE_EVAL_MPTR" => "instance_eval", + _ => panic!("unmapped quotient VM memory token {name}"), + }; + let needle = [ + "case {{ template_constants.quotient_vm.mem.", + field, + "|hex() }} { q_ptr := ", + name, + ] + .concat(); + assert!( + quotient_template.contains(&needle), + "quotient VM template missing memory token {name} template constant" + ); + } + assert_eq!(QUOTIENT_VM_SPEC.limb_count, layout::quotient_limb::LIMBS); + assert_eq!( + QUOTIENT_VM_SPEC.limb_pairwise_terms, + layout::quotient_limb::PAIRWISE_TERMS + ); + assert_eq!( + QUOTIENT_VM_SPEC.limb_pairwise_coeffs, + layout::quotient_limb::PAIRWISE_COEFFS + ); +} + +#[test] +fn quotient_vm_lengths_are_derived_from_opcode_spec() { + for spec in QUOTIENT_VM_SPEC.opcodes { + if spec.byte_len == 0 { + continue; + } + let bytes = vec![spec.opcode; spec.byte_len]; + assert_eq!( + quotient_op_len(&bytes, 0), + spec.byte_len, + "opcode {} length should come from QuotientVmSpec", + spec.name + ); + } +} + +#[test] +fn quotient_vm_safety_validator_rejects_malformed_programs() { + let underflow = + validate_quotient_program(&[Q_OP_ADD]).expect_err("ADD without operands must underflow"); + assert!( + underflow.contains("stack underflow"), + "unexpected underflow error: {underflow}" + ); + + let leak = + validate_quotient_program(&[Q_OP_PUSH_CONST_U8, 0, Q_OP_PUSH_CONST_U8, 0, Q_OP_FOLD_MAIN]) + .expect_err("fold with leaked spilled stack value must fail"); + assert!( + leak.contains("requires exactly 1 stack value"), + "unexpected leak error: {leak}" + ); + + let bad_token = validate_quotient_program(&[Q_OP_PUSH_MEM_TOKEN, 0xff, Q_OP_FOLD_MAIN]) + .expect_err("unknown memory token must fail"); + assert!( + bad_token.contains("unknown quotient VM memory token"), + "unexpected token error: {bad_token}" + ); + + let truncated = + validate_quotient_program(&[Q_OP_PUSH_CONST, 0]).expect_err("truncated operand must fail"); + assert!( + truncated.contains("truncated quotient VM"), + "unexpected truncation error: {truncated}" + ); +} + +#[test] +fn quotient_vm_safety_validator_accepts_byte_programs() { + let bytes = [Q_OP_PUSH_CONST_U8, 0, Q_OP_POW5, Q_OP_FOLD_MAIN]; + assert_eq!(validate_quotient_program(&bytes).unwrap(), 1); +} + +#[test] +fn normalized_yul_assignment_parser_tolerates_formatting_variants() { + assert_eq!( + yul_let_assignment(" let z:=addmod(a, b, r) "), + Some(("z".to_string(), "addmod(a, b, r)".to_string())) + ); + assert_eq!( + yul_addmod_assignment("let z := addmod ( a, mulmod(b, c, r), r )"), + Some(( + "z".to_string(), + "a".to_string(), + "mulmod(b, c, r)".to_string() + )) + ); + assert_eq!( + yul_mulmod_assignment("let z:=mulmod(a,b,r)"), + Some(("z".to_string(), "a".to_string(), "b".to_string())) + ); +} + +#[test] +fn field_negations_used_by_traces_are_canonical() { + let quotient_template = + include_str!("../../templates/partials/quotient_numerator/QuotientNumeratorBlock.yul"); + let evaluator_source = include_str!("quotient_numerator/yul_emit.rs"); + let pcs_source = include_str!("kzg/mod.rs"); + + for (name, source, needle) in [ + ( + "byte quotient VM negation", + quotient_template, + "q_top := addmod(0, sub(r, q_top), r)", + ), + ( + "structured quotient linearization expected eval", + quotient_template, + "let linearization_expected_eval := addmod(0, sub(r, mload({{ program.eval_numer_mptr|hex() }})), r)", + ), + ( + "legacy direct quotient linearization expected eval", + quotient_template, + "let linearization_expected_eval := addmod(0, sub(r, quotient_eval_numer), r)", + ), + ( + "native evaluator negation", + evaluator_source, + "addmod(0, sub(r, {var}), r)", + ), + ( + "final pairing negative v scalar", + pcs_source, + "addmod(0, sub(r, mload(V_MPTR)), r)", + ), + ] { + assert!( + source.contains(needle), + "{name} should canonicalize zero negations with addmod" + ); + } +} + +#[test] +fn quotient_helper_definitions_live_in_shared_partial() { + let verifier_template = include_str!("../../templates/contracts/Halo2Verifier.sol"); + let quotient_and_linearization = + include_str!("../../templates/partials/verifier/QuotientAndLinearization.yul"); + let quotient_evaluator_template = + include_str!("../../templates/contracts/Halo2QuotientEvaluator.sol"); + let quotient_helpers = + include_str!("../../templates/partials/quotient_numerator/QuotientHelpers.yul"); + + assert!( + quotient_and_linearization.contains( + "{%- when None %}\n {%- include \"partials/quotient_numerator/QuotientHelpers.yul\" %}\n {%- include \"partials/quotient_numerator/QuotientNumeratorBlock.yul\" %}" + ), + "monolithic verifier quotient path should include helpers beside the numerator block" + ); + assert!( + quotient_evaluator_template.contains( + "{%- include \"partials/quotient_numerator/QuotientHelpers.yul\" %}\n {%- include \"partials/quotient_numerator/QuotientNumeratorBlock.yul\" %}" + ), + "external evaluator should include helpers beside the numerator block" + ); + for helper in [ + "function q_pow5(", + "function q_limb7(", + "function q_limb7_wide(", + ] { + assert!( + quotient_helpers.contains(helper), + "quotient helper partial should define {helper}" + ); + assert!( + !verifier_template.contains(helper), + "main verifier template should not carry duplicate quotient helper body {helper}" + ); + assert!( + !quotient_evaluator_template.contains(helper), + "external evaluator template should not carry duplicate quotient helper body {helper}" + ); + } +} + +#[test] +fn batch_invert_handles_empty_and_singleton_ranges() { + let verifier_template = verifier_template_corpus(); + + assert!( + verifier_template.contains("let count_bytes := sub(mptr_end, mptr_start)"), + "batch inversion must compute the requested range length" + ); + assert!( + verifier_template.contains("if iszero(count_bytes) { leave }"), + "empty batch inversion ranges should be a no-op" + ); + assert!( + verifier_template.contains("if eq(count_bytes, 0x20)"), + "singleton batch inversion ranges need a dedicated path" + ); + assert!( + verifier_template.contains("if ret { mstore(mptr_start, mload(single_scratch)) }"), + "singleton batch inversion must store the single inverse in place" + ); +} + +#[test] +fn production_verifier_documents_revert_or_true_policy() { + let verifier_template = verifier_template_corpus(); + + assert!( + verifier_template.contains("success-or-revert"), + "verifyProof NatSpec must document invalid-proof failure semantics" + ); + assert!( + !verifier_template.contains("InvalidVerifierDependency"), + "constructor pinning removes per-call dependency preflight code" + ); + assert!( + !verifier_template.contains("return false;"), + "generated verifier should not mix false returns with revert-on-invalid semantics" + ); + assert!( + !verifier_template.contains("mstore(0x00, success)"), + "generated verifier must not return a false boolean on invalid proofs" + ); + assert!( + verifier_template.contains("function ec_pairing(success, lhs_mptr, rhs_mptr) -> ret") + && verifier_template + .contains("ret := success\n if iszero(ret) { leave }") + && verifier_template.contains( + "ret := and(ret, mload(scratch))\n if iszero(ret) { revert(0, 0) }\n ret := 1", + ), + "final pairing helper must revert on pairing failure and normalize success to one" + ); + assert!( + verifier_template + .contains("success := ec_pairing(success, PAIRING_RHS_MPTR, PAIRING_LHS_MPTR)"), + "final epilogue must route the pairing check through the reverting helper" + ); + assert!( + !verifier_template.contains("return(0x00, 0x20)\n {%- else %}"), + "trace and production epilogues must not diverge into false-return semantics" + ); + assert!( + verifier_template.contains("mstore(RETURN_MPTR, 1)\n return(RETURN_MPTR, 0x20)"), + "generated verifier must only return literal true after the reverting pairing helper" + ); +} + +#[test] +fn templates_do_not_write_solidity_reserved_memory_slots() { + let verifier_template = verifier_template_corpus(); + let quotient_template = include_str!("../../templates/contracts/Halo2QuotientEvaluator.sol"); + let quotient_helpers = + include_str!("../../templates/partials/quotient_numerator/QuotientHelpers.yul"); + let vk_template = include_str!("../../templates/contracts/Halo2VerifyingKey.sol"); + let pcs_source = include_str!("kzg/mod.rs"); + + for (name, source) in [ + ("Halo2Verifier.sol", verifier_template), + ("Halo2QuotientEvaluator.sol", quotient_template), + ("QuotientHelpers.yul", quotient_helpers), + ("Halo2VerifyingKey.sol", vk_template), + ] { + for needle in [ + "mstore(0,", + "mstore(0x00,", + "mstore(0x20,", + "mstore(0x40,", + "mstore(0x60,", + "mstore(add(0x00,", + "mstore(add(0x20,", + "mstore(add(0x40,", + "mstore(add(0x60,", + "mcopy(0x00,", + "mcopy(0x20,", + "mcopy(0x40,", + "mcopy(0x60,", + "calldatacopy(0x00,", + "calldatacopy(0x20,", + "calldatacopy(0x40,", + "calldatacopy(0x60,", + "return(0x00,", + ] { + assert!( + !source.contains(needle), + "{name} must not write or return from Solidity-reserved memory words: found {needle}" + ); + } + } + assert!( + !pcs_source.contains("mcopy(0x0,") + && !pcs_source.contains("mstore(0,") + && !pcs_source.contains(", 0x00, {G1_MSM_PAIR_BYTES:#x}") + && !pcs_source.contains(", 0x00, {G1ADD_INPUT_BYTES:#x}"), + "generated PCS helper scratch must not be rooted at memory 0" + ); +} + +#[test] +fn templates_use_planned_memory_slots_for_theta_and_commitment_layout() { + for (name, source) in [ + ("Halo2Verifier", verifier_template_corpus()), + ( + "Halo2QuotientEvaluator", + include_str!("../../templates/contracts/Halo2QuotientEvaluator.sol"), + ), + ] { + assert!( + !source.contains("theta_mptr +"), + "{name} should render named planned theta-relative slots" + ); + assert!( + !source.contains("comms_mptr_base +"), + "{name} should render named planned commitment bases" + ); + } +} + +#[test] +fn final_msm_pair_count_is_a_codegen_assertion() { + let source = include_str!("kzg/mod.rs"); + + assert!( + source.contains("pair_idx, final_msm_terms"), + "PCS emission should compare emitted final MSM pairs with the planned shape" + ); + assert!( + source.contains("final MSM input term count changed during emission"), + "PCS emission should fail loudly if final MSM shape and Yul emission diverge" + ); + assert!( + !source.contains("debug_assert_eq!(\n pair_idx, final_msm_terms"), + "final MSM shape mismatches must not be debug-only" + ); +} + +#[test] +fn verify_proof_natspec_requires_application_binding() { + let verifier_template = verifier_template_corpus(); + + for required in [ + "checks only that `proof` verifies for the supplied public", + "Application contracts must", + "state roots", + "program", + "expected IVC outputs", + "chain/domain separation", + ] { + assert!( + verifier_template.contains(required), + "verifyProof NatSpec must document application binding requirement: {required}" + ); + } + for required in [ + "/// @param proof Solidity-facing proof bytes", + "/// @param instances Public instance scalars", + "/// @return Always `true`", + ] { + assert!( + verifier_template.contains(required), + "verifyProof NatSpec must include ABI documentation: {required}" + ); + } +} + +#[test] +fn production_verifier_entrypoint_is_external_view() { + let verifier_template = verifier_template_corpus(); + + assert!( + verifier_template.contains(") external {%- if self.trace || self.gas_checkpoints %} returns (bool) {%- else %} view returns (bool) {%- endif %}"), + "generated verifyProof entrypoint should render as external view in production" + ); + assert!( + !verifier_template.contains(") public {%- if self.trace || self.gas_checkpoints %}"), + "production verifier should not expose the calldata entrypoint as public" + ); +} + +#[test] +fn midfall_comment_corpus_is_source_indexed_and_attributed() { + let corpus = include_str!("../../docs/reference/MIDFALL_PROOFS_COMMENT_CORPUS.md"); + let extractor = include_str!("../../scripts/extract_midfall_comments.py"); + + for required in [ + "# Midfall Proofs Comment Corpus", + "Rust source files: `57`", + "Comment lines: `3597`", + "## `plonk/verifier.rs`", + "## `plonk/linearization/verifier.rs`", + "## `poly/kzg/mod.rs`", + "## `transcript/implementors.rs`", + "SPDX-License-Identifier: Apache-2.0", + "License And Attribution", + ] { + assert!( + corpus.contains(required), + "Midfall comment corpus missing expected source-indexed content: {required}" + ); + } + + for required in [ + "COMMENT_RE", + "--check", + "--write", + "Git commit", + "CommentBlock", + ] { + assert!( + extractor.contains(required), + "comment extractor should remain deterministic/checkable: {required}" + ); + } +} + +#[test] +fn solidity_templates_have_required_natspec_surface() { + let verifier = verifier_template_corpus(); + let vk = include_str!("../../templates/contracts/Halo2VerifyingKey.sol"); + let quotient = include_str!("../../templates/contracts/Halo2QuotientEvaluator.sol"); + + for (name, source, contract) in [ + ("verifier", verifier, "contract Halo2Verifier"), + ("verifying key", vk, "contract Halo2VerifyingKey"), + ( + "quotient evaluator", + quotient, + "contract Halo2QuotientEvaluator", + ), + ] { + assert!( + source.contains("/// @title") + && source.contains("/// @notice") + && source.contains("/// @dev"), + "{name} template should have contract-level NatSpec" + ); + assert!( + source.contains(contract), + "{name} template should still declare {contract}" + ); + } + + for required in [ + "/// @notice Verifying-key contract address", + "checked at construction time", + "/// @notice Quotient evaluator contract", + "/// @param authorizedVk", + "/// @param authorizedQuotient", + "/// @return Always `true`", + ] { + assert!( + verifier.contains(required), + "verifier NatSpec missing required declaration docs: {required}" + ); + } + assert!( + vk.contains("/// @notice Deploy the verifying-key payload"), + "VK constructor should have NatSpec" + ); + assert!( + quotient.contains("/// @notice Evaluate the generated quotient numerator block"), + "quotient fallback should have NatSpec" + ); +} + +#[test] +fn midfall_comment_ports_reference_rust_sources() { + let verifier = verifier_template_corpus(); + let quotient = include_str!("../../templates/contracts/Halo2QuotientEvaluator.sol"); + let quotient_helpers = + include_str!("../../templates/partials/quotient_numerator/QuotientHelpers.yul"); + let numerator = + include_str!("../../templates/partials/quotient_numerator/QuotientNumeratorBlock.yul"); + let lowering = lowering_source_corpus(); + let evaluator = include_str!("quotient_numerator/yul_emit.rs"); + let protocol = include_str!("protocol/mod.rs"); + let pcs = include_str!("kzg/mod.rs"); + let spec = include_str!("../../docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md"); + + for required in [ + "midfall/proofs/src/plonk/verifier.rs", + "midfall/proofs/src/transcript/implementors.rs", + "midfall/proofs/src/poly/kzg/mod.rs", + ] { + assert!( + verifier.contains(required) || spec.contains(required), + "verifier docs should reference upstream Rust source: {required}" + ); + } + for required in [ + "midfall/proofs/src/plonk/mod.rs::partially_evaluate_identities", + "midfall/proofs/src/plonk/linearization/verifier.rs::compute_linearization_commitment", + "selectors do not appear as normal proof eval scalars", + ] { + assert!( + quotient.contains(required) + || quotient_helpers.contains(required) + || numerator.contains(required), + "quotient docs should carry adapted upstream comments: {required}" + ); + } + assert!( + verifier.contains("Hashable for G1Projective") + || spec.contains("Hashable for G1Projective"), + "transcript docs should reference upstream point hashing comments" + ); + assert!( + protocol.contains("counterpart of the iterator-heavy verifier"), + "protocol plan should document the Midfall verifier-flow port" + ); + for required in [ + "committed instances and normal (non-committed) instances", + "Fixed evals are query-based proof reads", + "absorbs this `transcript_repr` digest", + ] { + assert!( + lowering.contains(required), + "generator should carry adapted verifier comment: {required}" + ); + } + for required in [ + "Hash the prover's advice commitments", + "Read commitment(s) to the quotient polynomial h(X)=nu(X)/(X^n-1)", + "Queries corresponding to simple, multiplicative selectors need not be", + "omega^rotation*x", + ] { + assert!( + protocol.contains(required), + "protocol plan should carry adapted verifier comment: {required}" + ); + } + for required in [ + "partially_evaluate_identities", + "LogUp emitter", + "Permutation emitter", + "Trashcan emitter", + ] { + assert!( + evaluator.contains(required), + "quotient evaluator codegen should carry adapted verifier comment: {required}" + ); + } + assert!( + pcs.contains("Upstream comments ported here") + && pcs.contains("Sort point sets by ascending cardinality") + && pcs.contains("Sample a challenge x_3") + && pcs.contains("Scale z*pi - vG"), + "PCS emitter should document the KZG comment port" + ); +} + +#[test] +fn gas_checkpoints_are_debug_only_template_paths() { + let verifier_template = verifier_template_corpus(); + let lib_source = include_str!("../lib.rs"); + + assert!( + verifier_template.contains("{%- if self.gas_checkpoints %}\n // Section-boundary gas-attribution checkpoint"), + "gas checkpoint helper must be guarded by the gas-checkpoint template flag" + ); + assert_eq!( + verifier_template.matches("gas_checkpoint(").count(), + verifier_template.matches("{%- if self.gas_checkpoints").count(), + "every gas_checkpoint definition/call should have its own self.gas_checkpoints guard" + ); + assert!( + verifier_template.contains(") external {%- if self.trace || self.gas_checkpoints %} returns (bool) {%- else %} view returns (bool) {%- endif %}"), + "production renders should stay external view unless trace/gas logs are enabled" + ); + assert!( + lib_source.contains("pub const SOLIDITY_GAS_CHECKPOINTS_ENABLED"), + "gas checkpoints should remain an explicit feature flag" + ); + assert!( + lib_source.contains( + "pub const SOLIDITY_GAS_CHECKPOINTS_ENABLED: bool = cfg!(feature = \"solidity-gas-checkpoints\");" + ), + "default renders should allow gas checkpoints without trace/profiling logs" + ); + assert!( + lib_source.contains("RenderDiagnostics { gas_checkpoints: true, .. }"), + "docs should call out the explicit benchmarking render knob" + ); +} + +#[test] +fn trash_challenge_is_squeezed_even_without_trash_arguments() { + let verifier_template = verifier_template_corpus(); + let squeeze = verifier_template + .find("buf_len := squeeze_to(buf_len, TRASH_CHALLENGE_MPTR)") + .expect("trash challenge squeeze should be rendered"); + let trash_guard = verifier_template + .find("{%- if num_trashcans != 0 %}\n // ---- trashcans ----") + .expect("trashcan commitment reads should still be guarded"); + + assert!( + squeeze < trash_guard, + "Midnight squeezes trash_challenge unconditionally; only trashcan commitment reads may be guarded" + ); +} + +#[test] +fn truncated_challenge_comments_cover_x1_x4_power_masks() { + let verifier_template = verifier_template_corpus(); + let pcs_codegen = include_str!("kzg/mod.rs"); + + assert!( + verifier_template.contains("x1 and x4 remain full squeezed Fr words"), + "x3 squeeze comment must clarify that x1/x4 are handled by truncated powers" + ); + assert!( + verifier_template.contains("truncate(x1^i) and truncate(x4^i)"), + "verifier template should document the PCS power truncation rule" + ); + assert!( + pcs_codegen.contains("proofs/src/poly/kzg/mod.rs computes"), + "x1 power generation should point back to the Rust verifier source" + ); + assert!( + pcs_codegen.contains("power[i] = truncate(x1^i)"), + "x1 power generation should document truncated_powers(x1)" + ); + assert!( + pcs_codegen.contains("truncated_powers(x4)[i] = truncate(x4^i)"), + "x4 power generation should document truncated_powers(x4)" + ); + assert!( + pcs_codegen.contains("mstore(p, and(acc, {TRUNC_MASK_128}))"), + "x1 emitted powers must be masked under truncated-challenges" + ); + assert!( + pcs_codegen.contains("let x4_pow_{s} := and(x4_pow_full, {TRUNC_MASK_128})"), + "x4 emitted powers must be masked under truncated-challenges" + ); +} + +#[test] +fn generated_heavy_assemblies_keep_memory_safe_for_via_ir() { + for (name, source) in [ + ("Halo2Verifier.sol", verifier_template_corpus()), + ( + "Halo2QuotientEvaluator.sol", + include_str!("../../templates/contracts/Halo2QuotientEvaluator.sol"), + ), + ] { + assert!( + source.contains("assembly (\"memory-safe\")"), + "{name} needs memory-safe assembly annotations for solc via-IR stack allocation" + ); + } +} + +#[test] +fn generator_restriction_errors_are_typed() { + assert_eq!( + SolidityGenerator::SUPPORTED_COMMITTED_INSTANCE_COMMITMENT, + CommittedInstanceCommitmentKind::Identity + ); + assert_eq!( + GeneratorError::UnsupportedInstanceColumnShape { + total: 2, + committed: 0, + expected_committed: 1, + expected_non_committed: 1, + } + .to_string(), + "unsupported instance column shape: got total=2, committed=0, non_committed=2; expected exactly 1 identity-committed and 1 non-committed" + ); + assert_eq!( + GeneratorError::UnsupportedInstanceColumnShape { + total: 1, + committed: 2, + expected_committed: 1, + expected_non_committed: 1, + } + .to_string(), + "unsupported instance column shape: got total=1, committed=2, non_committed=invalid: committed 2 exceeds total 1; expected exactly 1 identity-committed and 1 non-committed" + ); + assert_eq!( + GeneratorError::RotatedInstanceQuery { + column: 1, + rotation: -1, + } + .to_string(), + "rotated instance query is not supported: column 1, rotation -1" + ); + assert_eq!( + GeneratorError::UnsupportedAccumulatorEncoding { + offset: 5, + num_limbs: 7, + num_limb_bits: 56, + num_instances: 14, + reason: "accumulator public-input tail exceeds num_instances", + } + .to_string(), + "unsupported accumulator encoding: offset=5, num_limbs=7, num_limb_bits=56, num_instances=14; accumulator public-input tail exceeds num_instances" + ); + let stale = ["not", " yet ", "implemented"].concat(); + assert!( + !include_str!("mod.rs").contains(&stale), + "unsupported verifier shapes should be surfaced as GeneratorError values" + ); +} + +#[test] +fn permutation_delta_literal_is_computed_from_field_constant() { + let computed = u256_string(fe_to_u256::(&Fq::DELTA)); + + assert_eq!(fr_delta_literal(), computed); + assert!( + !include_str!("mod.rs").contains(&computed), + "codegen source should not hard-code the current Fr::DELTA decimal/hex literal" + ); +} + +#[test] +fn verifier_template_omits_dead_constants_and_ec_helpers() { + let verifier_template = verifier_template_corpus(); + + for stale in [ + "FIRST_QUOTIENT_X_CPTR", + "LAST_QUOTIENT_X_CPTR", + "G1_SCALAR_MPTR", + "function ec_add_acc", + "function ec_mul_acc", + "function ec_add_tmp", + "function ec_mul_tmp", + ] { + assert!( + !verifier_template.contains(stale), + "production verifier template should not keep dead helper/constant: {stale}" + ); + } +} + +#[test] +fn expression_lowering_matches_quotient_vm_eval() { + let mut cs = ConstraintSystem::default(); + let advice = cs.advice_column(); + let fixed = cs.fixed_column(); + let instance = cs.instance_column(); + let challenge = cs.challenge_usable_after(FirstPhase); + cs.create_gate("typed lowering", |meta| { + let a = meta.query_advice(advice, Rotation::next()); + let f = meta.query_fixed(fixed, Rotation::prev()); + let i = meta.query_instance(instance, Rotation::cur()); + let c = meta.query_challenge(challenge); + let seven = Expression::Constant(Fq::from(7u64)); + Constraints::without_selector(vec![( + "typed lowering", + a.clone() * f.clone() + i.clone() * c.clone() + seven * a - f, + )]) + }); + let expr = cs.gates()[0].polynomials()[0].clone(); + + let mut values = HashMap::new(); + values.insert(0x100, Fq::from(11u64)); + values.insert(0x120, Fq::from(13u64)); + values.insert(0x140, Fq::from(17u64)); + values.insert(0x160, Fq::from(19u64)); + + let mut env = TestQuotientExpressionEnv::default(); + env.advice.insert( + (advice.index(), 1), + QuotientExpr::Mem(QuotientMem::Literal(0x100)), + ); + env.fixed.insert( + (fixed.index(), -1), + QuotientExpr::Mem(QuotientMem::Literal(0x120)), + ); + env.instance.insert( + (instance.index(), 0), + QuotientExpr::Mem(QuotientMem::Literal(0x140)), + ); + env.challenges.insert( + challenge.index(), + QuotientExpr::Mem(QuotientMem::Literal(0x160)), + ); + + let expected = expr.evaluate( + &|scalar| scalar, + &|_| panic!("test expression should not contain selectors"), + &|_query| values[&0x120], + &|query| { + assert_eq!(query.rotation(), Rotation::next()); + values[&0x100] + }, + &|query| { + assert_eq!(query.rotation(), Rotation::cur()); + values[&0x140] + }, + &|_| values[&0x160], + &|inner| -inner, + &|lhs, rhs| lhs + rhs, + &|lhs, rhs| lhs * rhs, + &|inner, scalar| inner * scalar, + ); + + let lowered = quotient_expr_from_expression(&env, &expr); + let mut builder = QuotientProgramBuilder::default(); + builder.emit_expr(&lowered); + let actual = eval_quotient_vm_for_test(&builder.bytes, &builder.consts, &values); + assert_eq!(actual, expected); +} + +#[test] +fn quotient_vm_lin7_matches_direct_expr_eval() { + let mut values = HashMap::new(); + let mut expr = QuotientExpr::Const(U256::ZERO); + for i in 0..7u32 { + let ptr = 0x300 + i * 0x20; + values.insert(ptr, Fq::from(11 + i as u64)); + expr = quotient_add_expr( + expr, + quotient_scale_expr( + Fq::from(3 + i as u64), + QuotientExpr::Mem(QuotientMem::Literal(ptr)), + ), + ); + } + + let expected = eval_quotient_expr_for_test(&expr, &values); + let mut builder = QuotientProgramBuilder::with_limb_vm_ops(true); + builder.emit_expr(&expr); + + assert_eq!(builder.bytes[0], Q_OP_LIN7); + assert_eq!( + eval_quotient_vm_for_test(&builder.bytes, &builder.consts, &values), + expected + ); +} + +#[test] +fn quotient_vm_bilin7_row_matches_direct_expr_eval() { + let lhs = 0x440; + let mut values = HashMap::new(); + values.insert(lhs, Fq::from(29u64)); + let mut expr = QuotientExpr::Const(U256::ZERO); + for i in 0..7u32 { + let rhs = 0x500 + i * 0x20; + values.insert(rhs, Fq::from(37 + i as u64)); + expr = quotient_add_expr( + expr, + quotient_scale_expr( + Fq::from(5 + i as u64), + quotient_mul_expr( + QuotientExpr::Mem(QuotientMem::Literal(lhs)), + QuotientExpr::Mem(QuotientMem::Literal(rhs)), + ), + ), + ); + } + + let expected = eval_quotient_expr_for_test(&expr, &values); + let mut builder = QuotientProgramBuilder::with_limb_vm_ops(true); + builder.emit_expr(&expr); + + assert_eq!(builder.bytes[0], Q_OP_BILIN7_ROW); + assert_eq!( + eval_quotient_vm_for_test(&builder.bytes, &builder.consts, &values), + expected + ); +} + +#[test] +fn quotient_vm_bilin7_pairwise_matches_direct_expr_eval() { + let lhs_base = 0x620; + let rhs_base = 0x820; + let mut values = HashMap::new(); + for i in 0..7u32 { + values.insert(lhs_base + i * 0x20, Fq::from(41 + i as u64)); + values.insert(rhs_base + i * 0x20, Fq::from(71 + i as u64)); + } + + let mut expr = QuotientExpr::Const(U256::ZERO); + for i in 0..7u32 { + for j in 0..7u32 { + expr = quotient_add_expr( + expr, + quotient_scale_expr( + Fq::from(9 + i as u64 + j as u64), + quotient_mul_expr( + QuotientExpr::Mem(QuotientMem::Literal(lhs_base + i * 0x20)), + QuotientExpr::Mem(QuotientMem::Literal(rhs_base + j * 0x20)), + ), + ), + ); + } + } + + let expected = eval_quotient_expr_for_test(&expr, &values); + let mut builder = QuotientProgramBuilder::with_limb_vm_ops(true); + builder.emit_expr(&expr); + + assert_eq!(builder.bytes[0], Q_OP_BILIN7_PAIRWISE); + assert_eq!( + eval_quotient_vm_for_test(&builder.bytes, &builder.consts, &values), + expected + ); +} + +#[test] +fn quotient_vm_pow5_matches_direct_expr_eval() { + let ptr = 0xa20; + let mut values = HashMap::new(); + values.insert(ptr, Fq::from(131u64)); + let base = QuotientExpr::Mem(QuotientMem::Literal(ptr)); + let expr = quotient_mul_expr( + quotient_mul_expr(base.clone(), base.clone()), + quotient_mul_expr(base.clone(), quotient_mul_expr(base.clone(), base)), + ); + + let expected = eval_quotient_expr_for_test(&expr, &values); + let mut builder = QuotientProgramBuilder::default(); + builder.emit_expr(&expr); + + assert_eq!(builder.bytes[3], Q_OP_POW5); + assert_eq!( + eval_quotient_vm_for_test(&builder.bytes, &builder.consts, &values), + expected + ); +} + +#[test] +fn quotient_vm_pow5_rejects_near_miss_product_shapes() { + let a_ptr = 0xa60; + let b_ptr = 0xa80; + let mut values = HashMap::new(); + values.insert(a_ptr, Fq::from(137u64)); + values.insert(b_ptr, Fq::from(139u64)); + + let a = QuotientExpr::Mem(QuotientMem::Literal(a_ptr)); + let b = QuotientExpr::Mem(QuotientMem::Literal(b_ptr)); + let a4_times_b = quotient_mul_expr( + quotient_mul_expr(quotient_mul_expr(a.clone(), a.clone()), a.clone()), + quotient_mul_expr(a, b), + ); + + let expected = eval_quotient_expr_for_test(&a4_times_b, &values); + let mut builder = QuotientProgramBuilder::default(); + builder.emit_expr(&a4_times_b); + + assert!( + !builder.bytes.contains(&Q_OP_POW5), + "a^4 * b must not be compressed as a POW5" + ); + assert_eq!( + eval_quotient_vm_for_test(&builder.bytes, &builder.consts, &values), + expected + ); +} + +#[test] +fn quotient_vm_limb_subshape_matches_direct_expr_eval() { + let mut values = HashMap::new(); + let mut expr = QuotientExpr::Const(U256::ZERO); + let residue = quotient_mul_expr( + QuotientExpr::Mem(QuotientMem::Literal(0xc00)), + quotient_mul_expr( + QuotientExpr::Mem(QuotientMem::Literal(0xc20)), + QuotientExpr::Mem(QuotientMem::Literal(0xc40)), + ), + ); + values.insert(0xc00, Fq::from(211u64)); + values.insert(0xc20, Fq::from(223u64)); + values.insert(0xc40, Fq::from(227u64)); + + for i in 0..7u32 { + let ptr = 0xb00 + i * 0x20; + values.insert(ptr, Fq::from(151 + i as u64)); + if i == 1 { + expr = quotient_add_expr(expr, QuotientExpr::Const(U256::from(9u64))); + } + if i == 4 { + expr = quotient_add_expr(expr, residue.clone()); + } + expr = quotient_add_expr( + expr, + quotient_scale_expr( + Fq::from(19 + i as u64), + QuotientExpr::Mem(QuotientMem::Literal(ptr)), + ), + ); + } + + let expected = eval_quotient_expr_for_test(&expr, &values); + let mut builder = QuotientProgramBuilder::with_limb_vm_ops(true); + builder.emit_expr(&expr); + + assert!( + builder.bytes.contains(&Q_OP_LIN7), + "larger affine sums should still extract LIN7 subshapes" + ); + assert_eq!( + eval_quotient_vm_for_test(&builder.bytes, &builder.consts, &values), + expected + ); +} + +#[test] +fn quotient_vm_limb_subshape_inside_conditional_product_matches_direct_expr_eval() { + let mut values = HashMap::new(); + let cond_ptr = 0xd00; + values.insert(cond_ptr, Fq::from(3u64)); + let mut inner = QuotientExpr::Const(U256::ZERO); + for i in 0..7u32 { + let ptr = 0xe00 + i * 0x20; + values.insert(ptr, Fq::from(251 + i as u64)); + if i == 2 { + inner = quotient_add_expr(inner, QuotientExpr::Const(U256::from(17u64))); + } + inner = quotient_add_expr( + inner, + quotient_scale_expr( + Fq::from(31 + i as u64), + QuotientExpr::Mem(QuotientMem::Literal(ptr)), + ), + ); + } + let expr = quotient_mul_expr(QuotientExpr::Mem(QuotientMem::Literal(cond_ptr)), inner); + + let expected = eval_quotient_expr_for_test(&expr, &values); + let mut builder = QuotientProgramBuilder::with_limb_vm_ops(true); + builder.emit_expr(&expr); + + assert!( + builder.bytes.contains(&Q_OP_MODARITH7), + "conditional affine factors should collapse into the fused mod-arith opcode" + ); + assert_eq!( + eval_quotient_vm_for_test(&builder.bytes, &builder.consts, &values), + expected + ); +} + +#[test] +fn quotient_vm_modarith7_mixed_affine_matches_direct_expr_eval() { + let lhs_base = 0xf00; + let rhs_base = 0x1100; + let lin_base = 0x1300; + let factored_lin_base = 0x1700; + let cond = 0x1500; + let scalar = 0x1520; + let product_lhs = 0x1540; + let product_rhs = 0x1560; + let mut values = HashMap::new(); + values.insert(cond, Fq::from(3u64)); + values.insert(scalar, Fq::from(5u64)); + values.insert(product_lhs, Fq::from(11u64)); + values.insert(product_rhs, Fq::from(13u64)); + for i in 0..7u32 { + values.insert(lhs_base + i * WORD_BYTES as u32, Fq::from(7 + i as u64)); + values.insert(rhs_base + i * WORD_BYTES as u32, Fq::from(17 + i as u64)); + values.insert(lin_base + i * WORD_BYTES as u32, Fq::from(29 + i as u64)); + values.insert( + factored_lin_base + i * WORD_BYTES as u32, + Fq::from(31 + i as u64), + ); + } + + let mut inner = QuotientExpr::Const(U256::from(41u64)); + for i in 0..7u32 { + inner = quotient_add_expr( + inner, + quotient_scale_expr( + Fq::from(43 + i as u64), + QuotientExpr::Mem(QuotientMem::Literal(lin_base + i * WORD_BYTES as u32)), + ), + ); + } + let mut factored_lin = QuotientExpr::Const(U256::ZERO); + for i in 0..7u32 { + factored_lin = quotient_add_expr( + factored_lin, + quotient_scale_expr( + Fq::from(47 + i as u64), + QuotientExpr::Mem(QuotientMem::Literal( + factored_lin_base + i * WORD_BYTES as u32, + )), + ), + ); + } + inner = quotient_add_expr( + inner, + quotient_mul_expr(QuotientExpr::Mem(QuotientMem::Literal(cond)), factored_lin), + ); + for i in 0..7u32 { + for j in 0..7u32 { + inner = quotient_add_expr( + inner, + quotient_scale_expr( + Fq::from(53 + i as u64 + j as u64), + quotient_mul_expr( + QuotientExpr::Mem(QuotientMem::Literal(lhs_base + i * WORD_BYTES as u32)), + QuotientExpr::Mem(QuotientMem::Literal(rhs_base + j * WORD_BYTES as u32)), + ), + ), + ); + } + } + inner = quotient_add_expr( + inner, + quotient_scale_expr( + Fq::from(97u64), + QuotientExpr::Mem(QuotientMem::Literal(scalar)), + ), + ); + inner = quotient_add_expr( + inner, + quotient_scale_expr( + Fq::from(101u64), + quotient_mul_expr( + QuotientExpr::Mem(QuotientMem::Literal(product_lhs)), + QuotientExpr::Mem(QuotientMem::Literal(product_rhs)), + ), + ), + ); + let expr = quotient_mul_expr(QuotientExpr::Mem(QuotientMem::Literal(cond)), inner); + + let expected = eval_quotient_expr_for_test(&expr, &values); + let mut builder = QuotientProgramBuilder::with_limb_vm_ops(true); + builder.emit_expr(&expr); + + assert_eq!(builder.bytes[0], Q_OP_MODARITH7); + assert_eq!(quotient_op_len(&builder.bytes, 0), builder.bytes.len()); + let (ops, mem_tokens) = quotient_program_usage(&builder.bytes); + assert_eq!(ops, vec![Q_OP_MODARITH7]); + assert!(mem_tokens.is_empty()); + assert_eq!( + eval_quotient_vm_for_test(&builder.bytes, &builder.consts, &values), + expected + ); +} + +#[test] +fn quotient_vm_modarith7_sparse_affine_product_matches_direct_expr_eval() { + let cond = 0x1800; + let linear = 0x1820; + let lhs0 = 0x1840; + let rhs0 = 0x1860; + let lhs1 = 0x1880; + let rhs1 = 0x18a0; + let factored0 = 0x18c0; + let factored1 = 0x18e0; + let mut values = HashMap::new(); + values.insert(cond, Fq::from(3u64)); + values.insert(linear, Fq::from(5u64)); + values.insert(lhs0, Fq::from(7u64)); + values.insert(rhs0, Fq::from(11u64)); + values.insert(lhs1, Fq::from(13u64)); + values.insert(rhs1, Fq::from(17u64)); + values.insert(factored0, Fq::from(19u64)); + values.insert(factored1, Fq::from(23u64)); + + let mut inner = QuotientExpr::Const(U256::from(19u64)); + inner = quotient_add_expr( + inner, + quotient_scale_expr( + Fq::from(23u64), + QuotientExpr::Mem(QuotientMem::Literal(linear)), + ), + ); + inner = quotient_add_expr( + inner, + quotient_scale_expr( + Fq::from(29u64), + quotient_mul_expr( + QuotientExpr::Mem(QuotientMem::Literal(lhs0)), + QuotientExpr::Mem(QuotientMem::Literal(rhs0)), + ), + ), + ); + inner = quotient_add_expr( + inner, + quotient_scale_expr( + Fq::from(31u64), + quotient_mul_expr( + QuotientExpr::Mem(QuotientMem::Literal(lhs1)), + QuotientExpr::Mem(QuotientMem::Literal(rhs1)), + ), + ), + ); + let factored = quotient_add_expr( + QuotientExpr::Const(U256::from(37u64)), + quotient_add_expr( + quotient_scale_expr( + Fq::from(41u64), + QuotientExpr::Mem(QuotientMem::Literal(factored0)), + ), + quotient_scale_expr( + Fq::from(43u64), + QuotientExpr::Mem(QuotientMem::Literal(factored1)), + ), + ), + ); + inner = quotient_add_expr( + inner, + quotient_scale_expr( + Fq::from(47u64), + quotient_mul_expr(QuotientExpr::Mem(QuotientMem::Literal(cond)), factored), + ), + ); + let expr = quotient_mul_expr(QuotientExpr::Mem(QuotientMem::Literal(cond)), inner); + + let expected = eval_quotient_expr_for_test(&expr, &values); + let mut builder = QuotientProgramBuilder::with_limb_vm_ops(true); + builder.emit_expr(&expr); + + assert_eq!(builder.bytes[0], Q_OP_MODARITH7); + assert_eq!(quotient_op_len(&builder.bytes, 0), builder.bytes.len()); + let (ops, mem_tokens) = quotient_program_usage(&builder.bytes); + assert_eq!(ops, vec![Q_OP_MODARITH7]); + assert!(mem_tokens.is_empty()); + assert_eq!( + eval_quotient_vm_for_test(&builder.bytes, &builder.consts, &values), + expected + ); +} + +#[test] +fn unmatched_limb_shape_falls_back_to_existing_vm_ops() { + let mut values = HashMap::new(); + let mut expr = QuotientExpr::Const(U256::ZERO); + for i in 0..6u32 { + let ptr = 0x980 + i * 0x20; + values.insert(ptr, Fq::from(101 + i as u64)); + expr = quotient_add_expr( + expr, + quotient_scale_expr( + Fq::from(17 + i as u64), + QuotientExpr::Mem(QuotientMem::Literal(ptr)), + ), + ); + } + + let expected = eval_quotient_expr_for_test(&expr, &values); + let mut builder = QuotientProgramBuilder::with_limb_vm_ops(true); + builder.emit_expr(&expr); + + assert_ne!(builder.bytes[0], Q_OP_LIN7); + assert_eq!( + eval_quotient_vm_for_test(&builder.bytes, &builder.consts, &values), + expected + ); +} + +#[test] +fn quotient_program_usage_tracks_byte_ops_and_tokens() { + let bytes = vec![ + Q_OP_PUSH_MEM_TOKEN, + Q_MEM_THETA, + Q_OP_RUN_ADD_MUL_CONST_U8_MEM_U16, + 0x00, + 0x02, + 0x00, + 0x20, + 0x03, + 0x00, + 0x40, + 0x04, + Q_OP_FOLD_MAIN, + ]; + + let (ops, mem_tokens) = quotient_program_usage(&bytes); + + assert!(ops.contains(&Q_OP_PUSH_MEM_TOKEN)); + assert!(ops.contains(&Q_OP_RUN_ADD_MUL_CONST_U8_MEM_U16)); + assert!(ops.contains(&Q_OP_FOLD_MAIN)); + assert!(!ops.contains(&Q_OP_ADD_MUL_CONST_U8_MEM_U16)); + assert_eq!(mem_tokens, vec![Q_MEM_THETA]); +} + +/// Test helper for constructing quotient addition expressions. +fn quotient_add_expr(lhs: QuotientExpr, rhs: QuotientExpr) -> QuotientExpr { + QuotientExpr::Add(Box::new(lhs), Box::new(rhs)) +} + +/// Test helper for constructing quotient multiplication expressions. +fn quotient_mul_expr(lhs: QuotientExpr, rhs: QuotientExpr) -> QuotientExpr { + QuotientExpr::Mul(Box::new(lhs), Box::new(rhs)) +} + +/// Test helper for attaching an Fr coefficient to a quotient expression. +fn quotient_scale_expr(coeff: Fq, expr: QuotientExpr) -> QuotientExpr { + QuotientExpr::Mul( + Box::new(QuotientExpr::Const(fe_to_u256::(&coeff))), + Box::new(expr), + ) +} + +/// Construct a synthetic identity with a zero expression. +fn test_quotient_identity( + global_index: usize, + source: QuotientIdentitySource, + target: QuotientTarget, +) -> QuotientIdentity { + QuotientIdentity { + meta: QuotientIdentityMetadata { + global_index, + source, + }, + lines: Vec::new(), + var: "eval".to_string(), + target, + expr: QuotientExpr::Const(U256::ZERO), + } +} + +/// Construct a synthetic identity backed by an explicit expression. +fn test_quotient_identity_with_expr( + global_index: usize, + target: QuotientTarget, + expr: QuotientExpr, +) -> QuotientIdentity { + QuotientIdentity { + meta: QuotientIdentityMetadata { + global_index, + source: QuotientIdentitySource::Gate { + gate_index: global_index, + gate_name: format!("synthetic_gate_{global_index}"), + constraint_index: 0, + constraint_name: "synthetic_constraint".to_string(), + polynomial_index: 0, + }, + }, + lines: Vec::new(), + var: format!("eval_{global_index}"), + target, + expr, + } +} + +/// Build a quotient execution plan fixture while deriving selector fold data. +fn test_quotient_execution_plan( + expected: Vec, + inline_identities: Vec, + items: Vec, + native_identities: Vec, + native_permutation_identities: Vec, + native_lookup_identities: Vec, + structured_tail_identities: Vec, +) -> QuotientProgramPlan { + let selector_count = expected + .iter() + .filter_map(|identity| match identity.target { + QuotientTarget::Main => None, + QuotientTarget::Selector(selector_idx) => Some(selector_idx + 1), + }) + .max() + .unwrap_or(0); + let has_native_permutation = + items.iter().any(|item| matches!(item, QuotientProgramItem::NativePermutation)); + let has_native_lookup = + items.iter().any(|item| matches!(item, QuotientProgramItem::NativeLookup)); + QuotientProgramPlan { + inline_identities, + items, + native_identities, + native_permutation_identities, + native_lookup_identities, + structured_tail_identities, + sorted_simple: (0..selector_count).collect(), + has_native_permutation, + has_native_lookup, + selector_fold: VerifierBuildInputs::selector_fold_plan(&expected, selector_count), + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum TestQuotientBytecodeEvent { + Fold(QuotientTarget), + NativePermutation, + NativeLookup, + NativeIdentity(usize), +} + +/// Extract high-level fold/native events from test bytecode. +fn quotient_bytecode_events_for_test(bytes: &[u8]) -> Vec { + let mut events = Vec::new(); + let mut idx = 0usize; + while idx < bytes.len() { + match bytes[idx] { + Q_OP_FOLD_MAIN => { + events.push(TestQuotientBytecodeEvent::Fold(QuotientTarget::Main)); + idx += 1; + } + Q_OP_FOLD_SELECTOR => { + events.push(TestQuotientBytecodeEvent::Fold(QuotientTarget::Selector( + bytes[idx + 1] as usize, + ))); + idx += 4; + } + Q_OP_NATIVE_PERMUTATION => { + events.push(TestQuotientBytecodeEvent::NativePermutation); + idx += 1; + } + Q_OP_NATIVE_LOOKUP => { + events.push(TestQuotientBytecodeEvent::NativeLookup); + idx += 1; + } + Q_OP_NATIVE_IDENTITY => { + events.push(TestQuotientBytecodeEvent::NativeIdentity( + read_u16(bytes, idx + 1) as usize, + )); + idx += 3; + } + op => { + let len = quotient_opcode_byte_len(op) + .unwrap_or_else(|| panic!("dynamic test opcode {op:#x} at byte {idx}")); + idx += len; + } + } + } + events +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct TestIdentityTraceEntry { + global_index: usize, + target: QuotientTarget, + value: Fq, + y_power: usize, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct TestLinearizationArtifacts { + identity_trace: Vec, + main_numerator: Fq, + selector_buckets: Vec, + quotient_expected_eval: Fq, +} + +/// Small exponentiation helper for expected linearization powers. +fn fq_pow(base: Fq, exp: usize) -> Fq { + (0..exp).fold(Fq::ONE, |acc, _| acc * base) +} + +/// Evaluate expected quotient linearization artifacts directly from identities. +fn expected_linearization_artifacts_for_test( + identities: &[QuotientIdentity], + mem: &HashMap, + y: Fq, + selector_count: usize, +) -> TestLinearizationArtifacts { + let mut identity_trace = Vec::with_capacity(identities.len()); + let mut main_numerator = Fq::ZERO; + let mut selector_buckets = vec![Fq::ZERO; selector_count]; + let total = identities.len(); + + for identity in identities { + let value = eval_quotient_expr_for_test(&identity.expr, mem); + let y_power = total - 1 - identity.meta.global_index; + let scaled = value * fq_pow(y, y_power); + match identity.target { + QuotientTarget::Main => main_numerator += scaled, + QuotientTarget::Selector(selector_idx) => selector_buckets[selector_idx] += scaled, + } + identity_trace.push(TestIdentityTraceEntry { + global_index: identity.meta.global_index, + target: identity.target, + value, + y_power, + }); + } + + TestLinearizationArtifacts { + identity_trace, + main_numerator, + selector_buckets, + quotient_expected_eval: -main_numerator, + } +} + +/// Interpret folded quotient bytecode into the same artifacts the verifier +/// uses. +fn eval_quotient_program_artifacts_for_test( + bytes: &[u8], + consts: &[U256], + mem: &HashMap, + y: Fq, + selector_tail_exponents: &[usize], +) -> TestLinearizationArtifacts { + let mut idx = 0usize; + let mut identity_start = 0usize; + let mut main_numerator = Fq::ZERO; + let mut selector_buckets = vec![Fq::ZERO; selector_tail_exponents.len()]; + let mut identity_trace = Vec::new(); + + while idx < bytes.len() { + match bytes[idx] { + Q_OP_FOLD_MAIN => { + let value = eval_quotient_vm_for_test(&bytes[identity_start..idx], consts, mem); + main_numerator = main_numerator * y + value; + identity_trace.push(TestIdentityTraceEntry { + global_index: identity_trace.len(), + target: QuotientTarget::Main, + value, + y_power: 0, + }); + idx += 1; + identity_start = idx; + } + Q_OP_FOLD_SELECTOR => { + let value = eval_quotient_vm_for_test(&bytes[identity_start..idx], consts, mem); + let selector_idx = bytes[idx + 1] as usize; + let gap = read_u16(bytes, idx + 2) as usize; + main_numerator *= y; + selector_buckets[selector_idx] = + selector_buckets[selector_idx] * fq_pow(y, gap) + value; + identity_trace.push(TestIdentityTraceEntry { + global_index: identity_trace.len(), + target: QuotientTarget::Selector(selector_idx), + value, + y_power: 0, + }); + idx += 4; + identity_start = idx; + } + op => { + let len = quotient_opcode_byte_len(op) + .unwrap_or_else(|| panic!("dynamic test opcode {op:#x} at byte {idx}")); + idx += len; + } + } + } + + assert_eq!(identity_start, bytes.len(), "program ended mid-identity"); + let total = identity_trace.len(); + for entry in &mut identity_trace { + entry.y_power = total - 1 - entry.global_index; + } + for (bucket, tail) in selector_buckets.iter_mut().zip(selector_tail_exponents) { + *bucket *= fq_pow(y, *tail); + } + + TestLinearizationArtifacts { + identity_trace, + main_numerator, + selector_buckets, + quotient_expected_eval: -main_numerator, + } +} + +/// Evaluate the typed quotient expression tree for tests. +fn eval_quotient_expr_for_test(expr: &QuotientExpr, mem: &HashMap) -> Fq { + match expr { + QuotientExpr::Const(value) => fq_from_u256(*value), + QuotientExpr::Mem(QuotientMem::Literal(ptr)) => mem[ptr], + QuotientExpr::Mem(QuotientMem::Token(_)) + | QuotientExpr::Mem(QuotientMem::TokenOffset(_, _)) => { + panic!("test expressions use literal memory only") + } + QuotientExpr::Add(lhs, rhs) => { + eval_quotient_expr_for_test(lhs, mem) + eval_quotient_expr_for_test(rhs, mem) + } + QuotientExpr::Mul(lhs, rhs) => { + eval_quotient_expr_for_test(lhs, mem) * eval_quotient_expr_for_test(rhs, mem) + } + QuotientExpr::Neg(inner) => -eval_quotient_expr_for_test(inner, mem), + } +} + +/// Minimal Rust interpreter for quotient VM opcodes used by regression tests. +fn eval_quotient_vm_for_test(bytes: &[u8], consts: &[U256], mem: &HashMap) -> Fq { + let mut idx = 0usize; + let mut stack: Vec = Vec::new(); + + while idx < bytes.len() { + match bytes[idx] { + Q_OP_PUSH_CONST => { + let slot = read_u16(bytes, idx + 1) as usize; + stack.push(fq_from_u256(consts[slot])); + idx += 3; + } + Q_OP_PUSH_CONST_U8 => { + let slot = bytes[idx + 1] as usize; + stack.push(fq_from_u256(consts[slot])); + idx += 2; + } + Q_OP_PUSH_MEM_U16 => { + let ptr = read_u16(bytes, idx + 1) as u32; + stack.push(mem[&ptr]); + idx += 3; + } + Q_OP_PUSH_MEM_LITERAL => { + let ptr = read_u32(bytes, idx + 1); + stack.push(mem[&ptr]); + idx += 5; + } + Q_OP_ADD => { + let rhs = stack.pop().expect("rhs"); + let lhs = stack.pop().expect("lhs"); + stack.push(lhs + rhs); + idx += 1; + } + Q_OP_MUL => { + let rhs = stack.pop().expect("rhs"); + let lhs = stack.pop().expect("lhs"); + stack.push(lhs * rhs); + idx += 1; + } + Q_OP_NEG => { + let value = stack.pop().expect("value"); + stack.push(-value); + idx += 1; + } + Q_OP_POW5 => { + let value = stack.pop().expect("value"); + let value2 = value * value; + stack.push(value * value2 * value2); + idx += 1; + } + Q_OP_ADD_CONST_U8 => { + let slot = bytes[idx + 1] as usize; + let acc = stack.pop().expect("acc"); + stack.push(acc + fq_from_u256(consts[slot])); + idx += 2; + } + Q_OP_MUL_CONST_U8 => { + let slot = bytes[idx + 1] as usize; + let acc = stack.pop().expect("acc"); + stack.push(acc * fq_from_u256(consts[slot])); + idx += 2; + } + Q_OP_ADD_CONST => { + let slot = read_u16(bytes, idx + 1) as usize; + let acc = stack.pop().expect("acc"); + stack.push(acc + fq_from_u256(consts[slot])); + idx += 3; + } + Q_OP_MUL_CONST => { + let slot = read_u16(bytes, idx + 1) as usize; + let acc = stack.pop().expect("acc"); + stack.push(acc * fq_from_u256(consts[slot])); + idx += 3; + } + Q_OP_ADD_MEM_U16 => { + let ptr = read_u16(bytes, idx + 1) as u32; + let acc = stack.pop().expect("acc"); + stack.push(acc + mem[&ptr]); + idx += 3; + } + Q_OP_MUL_MEM_U16 => { + let ptr = read_u16(bytes, idx + 1) as u32; + let acc = stack.pop().expect("acc"); + stack.push(acc * mem[&ptr]); + idx += 3; + } + Q_OP_ADD_MUL_MEM_MEM_CONST_U8 => { + let lhs = read_u16(bytes, idx + 1) as u32; + let rhs = read_u16(bytes, idx + 3) as u32; + let slot = bytes[idx + 5] as usize; + let acc = stack.pop().expect("acc"); + stack.push(acc + mem[&lhs] * mem[&rhs] * fq_from_u256(consts[slot])); + idx += 6; + } + Q_OP_ADD_MUL_CONST_U8_MEM_U16 => { + let ptr = read_u16(bytes, idx + 1) as u32; + let slot = bytes[idx + 3] as usize; + let acc = stack.pop().expect("acc"); + stack.push(acc + fq_from_u256(consts[slot]) * mem[&ptr]); + idx += 4; + } + Q_OP_ADD_MUL_MEM_MEM => { + let lhs = read_u16(bytes, idx + 1) as u32; + let rhs = read_u16(bytes, idx + 3) as u32; + let acc = stack.pop().expect("acc"); + stack.push(acc + mem[&lhs] * mem[&rhs]); + idx += 5; + } + Q_OP_LIN7 => { + let mut acc = Fq::ZERO; + idx += 1; + for _ in 0..7 { + let slot = bytes[idx] as usize; + let ptr = read_u16(bytes, idx + 1) as u32; + acc += fq_from_u256(consts[slot]) * mem[&ptr]; + idx += 3; + } + stack.push(acc); + } + Q_OP_BILIN7_ROW => { + let lhs = read_u16(bytes, idx + 1) as u32; + let lhs_value = mem[&lhs]; + let mut acc = Fq::ZERO; + idx += 3; + for _ in 0..7 { + let slot = bytes[idx] as usize; + let rhs = read_u16(bytes, idx + 1) as u32; + acc += lhs_value * mem[&rhs] * fq_from_u256(consts[slot]); + idx += 3; + } + stack.push(acc); + } + Q_OP_BILIN7_PAIRWISE => { + let lhs_base = read_u16(bytes, idx + 1) as u32; + let rhs_base = read_u16(bytes, idx + 3) as u32; + let coeff_idx = idx + 5; + let mut acc = Fq::ZERO; + for i in 0..7 { + let lhs = mem[&(lhs_base + i * 0x20)]; + for j in 0..7 { + let rhs = mem[&(rhs_base + j * 0x20)]; + let slot = bytes[coeff_idx + i as usize + j as usize] as usize; + acc += lhs * rhs * fq_from_u256(consts[slot]); + } + } + idx += 18; + stack.push(acc); + } + Q_OP_MODARITH7 => { + idx += 1; + let flags = bytes[idx]; + idx += 1; + let cond = if flags & 0x01 != 0 { + let ptr = read_u16(bytes, idx) as u32; + idx += 2; + Some(ptr) + } else { + None + }; + + let mut acc = Fq::ZERO; + if flags & 0x02 != 0 { + let slot = bytes[idx] as usize; + idx += 1; + acc += fq_from_u256(consts[slot]); + } + + let lin_count = bytes[idx] as usize; + let row_count = bytes[idx + 1] as usize; + let pairwise_count = bytes[idx + 2] as usize; + let mem_count = bytes[idx + 3] as usize; + let product_count = bytes[idx + 4] as usize; + idx += 5; + + for _ in 0..lin_count { + for _ in 0..7 { + let slot = bytes[idx] as usize; + let ptr = read_u16(bytes, idx + 1) as u32; + acc += fq_from_u256(consts[slot]) * mem[&ptr]; + idx += 3; + } + } + for _ in 0..row_count { + let lhs = read_u16(bytes, idx) as u32; + let lhs_value = mem[&lhs]; + idx += 2; + for _ in 0..7 { + let slot = bytes[idx] as usize; + let rhs = read_u16(bytes, idx + 1) as u32; + acc += lhs_value * mem[&rhs] * fq_from_u256(consts[slot]); + idx += 3; + } + } + for _ in 0..pairwise_count { + let lhs_base = read_u16(bytes, idx) as u32; + let rhs_base = read_u16(bytes, idx + 2) as u32; + idx += 4; + let coeff_idx = idx; + idx += 13; + for i in 0..7u32 { + let lhs = mem[&(lhs_base + i * WORD_BYTES as u32)]; + for j in 0..7u32 { + let rhs = mem[&(rhs_base + j * WORD_BYTES as u32)]; + let slot = bytes[coeff_idx + i as usize + j as usize] as usize; + acc += lhs * rhs * fq_from_u256(consts[slot]); + } + } + } + for _ in 0..mem_count { + let slot = bytes[idx] as usize; + let ptr = read_u16(bytes, idx + 1) as u32; + acc += fq_from_u256(consts[slot]) * mem[&ptr]; + idx += 3; + } + for _ in 0..product_count { + let slot = bytes[idx] as usize; + let lhs = read_u16(bytes, idx + 1) as u32; + let rhs = read_u16(bytes, idx + 3) as u32; + acc += fq_from_u256(consts[slot]) * mem[&lhs] * mem[&rhs]; + idx += 5; + } + + if let Some(cond) = cond { + acc *= mem[&cond]; + } + stack.push(acc); + } + op => panic!("unsupported test quotient VM op {op:#x} at byte {idx}"), + } + } + + assert_eq!(stack.len(), 1, "test VM should leave one result"); + stack.pop().unwrap() +} + +/// Convert a canonical U256 test literal into Fr. +fn fq_from_u256(value: U256) -> Fq { + let bytes = value.to_le_bytes::<32>(); + let repr = ::Repr::from(bytes); + Option::::from(Fq::from_repr(repr)).expect("canonical field element") +} diff --git a/proofs/solidity-verifier/src/lowering/vk.rs b/proofs/solidity-verifier/src/lowering/vk.rs new file mode 100644 index 000000000..480b68c37 --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/vk.rs @@ -0,0 +1,443 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Verifying-key payload generation for a concrete verifier build. +//! +//! This module serializes the concrete Halo2 verifying key, quotient constants, +//! compact VM program, selector tables, and payload section map consumed by the +//! generated verifier contracts. + +use ff::Field; +use group::{prime::PrimeCurveAffine, Curve}; +use itertools::chain; +use midnight_curves::{Fq, G1Affine, G1Projective, G2Affine}; +use ruint::aliases::U256; + +use crate::lowering::{ + abi::{ProofCalldataLayout, TranscriptBufferLayout}, + encoding::{fe_to_u256, g1_to_u256s, g2_to_u256s, ConstraintSystemMeta, Data, Ptr}, + kzg, layout, + layout::{ + memory::{ + VerifierMemoryLayout, VerifierMemoryLayoutConfig, VkConstructorMemoryLayout, G1_BYTES, + WORD_BYTES, + }, + vk_payload::{PackedProgramCodec, PayloadSectionKind, VkPayloadLayout}, + }, + render::{Halo2VerifyingKey, VK_RUNTIME_PREFIX_LEN}, + VerifierBuildInputs, +}; + +impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { + /// Generate the VK payload before compact quotient constants/program data. + fn generate_base_vk(&self) -> Halo2VerifyingKey { + let constants: Vec<(&'static str, U256)>; + { + use layout::VkHeaderSlot as Slot; + + let domain = self.vk.get_domain(); + // BLS12-381 scalar Fq is 32 bytes wide (256 bits) so the same + // little-endian-to-u256 conversion that worked for BN254 Fr + // also works here: the verifier reads each scalar from + // calldata into a single 32-byte word. + // `plonk/verifier.rs` hashes the verifying key into the + // transcript before reading prover data. The Solidity template + // absorbs this `transcript_repr` digest as its first transcript + // word to preserve the Rust verifier's Fiat-Shamir prefix. + let vk_digest = fe_to_u256::(&self.vk.transcript_repr()); + let num_instances = U256::from(self.num_instances); + let k = U256::from(domain.k()); + let n_inv = fe_to_u256::(&Fq::from(1u64 << domain.k()).invert().unwrap()); + let omega = fe_to_u256::(&domain.get_omega()); + let omega_inv = fe_to_u256::(&domain.get_omega_inv()); + let omega_inv_to_l = { + let l = self.meta.rotation_last.unsigned_abs() as u64; + fe_to_u256::(&domain.get_omega_inv().pow_vartime([l])) + }; + let has_accumulator = U256::from(self.acc_encoding.is_some() as usize); + let acc_offset = self + .acc_encoding + .map(|acc_encoding| U256::from(acc_encoding.offset)) + .unwrap_or_default(); + let num_acc_limbs = self + .acc_encoding + .map(|acc_encoding| U256::from(acc_encoding.num_limbs)) + .unwrap_or_default(); + let num_acc_limb_bits = self + .acc_encoding + .map(|acc_encoding| U256::from(acc_encoding.num_limb_bits)) + .unwrap_or_default(); + // EIP-2537 padded encodings come from `g1_to_u256s` / `g2_to_u256s` + // (4 / 8 u256 words respectively). We cannot read `params.g[0]` + // directly (the field is crate-private in midnight-proofs), so + // we use the canonical BLS12-381 generator. + let g1_pt: G1Affine = G1Affine::generator(); + let g2_pt: G2Affine = self.params.g2().to_affine(); + let neg_s_g2_pt: G2Affine = (-self.params.s_g2()).to_affine(); + let g1 = g1_to_u256s(g1_pt); + let g2 = g2_to_u256s(g2_pt); + let neg_s_g2 = g2_to_u256s(neg_s_g2_pt); + + let mut header = layout::VkHeaderLayout::builder(); + header.scalar(Slot::VkDigest, "vk_digest", vk_digest).unwrap(); + header.scalar(Slot::NumInstances, "num_instances", num_instances).unwrap(); + header.scalar(Slot::K, "k", k).unwrap(); + header.scalar(Slot::NInv, "n_inv", n_inv).unwrap(); + header.scalar(Slot::Omega, "omega", omega).unwrap(); + header.scalar(Slot::OmegaInv, "omega_inv", omega_inv).unwrap(); + header.scalar(Slot::OmegaInvToL, "omega_inv_to_l", omega_inv_to_l).unwrap(); + header.scalar(Slot::HasAccumulator, "has_accumulator", has_accumulator).unwrap(); + header.scalar(Slot::AccOffset, "acc_offset", acc_offset).unwrap(); + header.scalar(Slot::NumAccLimbs, "num_acc_limbs", num_acc_limbs).unwrap(); + header + .scalar(Slot::NumAccLimbBits, "num_acc_limb_bits", num_acc_limb_bits) + .unwrap(); + header + .g1( + Slot::G1Base, + ["g1_x_hi", "g1_x_lo", "g1_y_hi", "g1_y_lo"], + g1, + ) + .unwrap(); + header + .g2( + Slot::G2Base, + [ + "g2_x_c0_hi", + "g2_x_c0_lo", + "g2_x_c1_hi", + "g2_x_c1_lo", + "g2_y_c0_hi", + "g2_y_c0_lo", + "g2_y_c1_hi", + "g2_y_c1_lo", + ], + g2, + ) + .unwrap(); + header + .g2( + Slot::NegSG2Base, + [ + "neg_s_g2_x_c0_hi", + "neg_s_g2_x_c0_lo", + "neg_s_g2_x_c1_hi", + "neg_s_g2_x_c1_lo", + "neg_s_g2_y_c0_hi", + "neg_s_g2_y_c0_lo", + "neg_s_g2_y_c1_hi", + "neg_s_g2_y_c1_lo", + ], + neg_s_g2, + ) + .unwrap(); + constants = + header.finish().unwrap_or_else(|err| panic!("invalid VK header layout: {err}")); + } + + // Convert each commitment from G1Projective to G1Affine before + // EIP-2537 packing. + let to_affine = |g: &G1Projective| -> G1Affine { g.to_affine() }; + let fixed_comms: Vec<_> = chain![self.vk.fixed_commitments()] + .map(to_affine) + .map(g1_to_u256s) + .map(|[a, b, c, d]| (a, b, c, d)) + .collect(); + let permutation_comms: Vec<_> = chain![self.vk.permutation().commitments()] + .map(to_affine) + .map(g1_to_u256s) + .map(|[a, b, c, d]| (a, b, c, d)) + .collect(); + let constructor_payload_len = + constants.len() * WORD_BYTES + (fixed_comms.len() + permutation_comms.len()) * G1_BYTES; + let constructor_memory = + VkConstructorMemoryLayout::new(constructor_payload_len + VK_RUNTIME_PREFIX_LEN); + constructor_memory + .validate() + .unwrap_or_else(|err| panic!("invalid VK constructor memory layout: {err}")); + Halo2VerifyingKey { + constructor_payload_mptr: constructor_memory.payload_mptr, + constants, + fixed_comms, + permutation_comms, + quotient_const_offset_words: None, + quotient_const_words: 0, + quotient_program_offset_words: None, + quotient_program_words: 0, + } + } + + /// Generate the final VK payload, including compact quotient VM data. + /// + /// The quotient program depends on memory addresses, and memory addresses + /// depend on the final VK length. This method reserves zero-filled quotient + /// sections first, rebuilds against the final layout, and then fills the + /// sections. Assertions catch any non-convergent program size change. + pub(crate) fn generate_vk(&self) -> Halo2VerifyingKey { + let mut vk = self.generate_base_vk(); + + let header_words = vk.constants.len(); + let mut quotient_const_words = 0usize; + let mut quotient_program_words = 0usize; + let mut quotient_program_build = None; + + for _ in 0..8 { + vk.constants.truncate(header_words); + vk.constants + .extend((0..quotient_const_words).map(|_| ("quotient_const", U256::ZERO))); + vk.constants + .extend((0..quotient_program_words).map(|_| ("quotient_program", U256::ZERO))); + vk.quotient_const_offset_words = None; + vk.quotient_const_words = 0; + vk.quotient_program_offset_words = None; + vk.quotient_program_words = 0; + + let (_, meta, data, _) = self.meta_data_for_stable_static_layout(&vk); + let (candidate, _) = self.compact_quotient_program_for(&meta, &data); + let candidate_const_words = candidate.consts.len(); + let candidate_program_words = + PackedProgramCodec::word_len_for_bytes(candidate.bytes.len()); + + if candidate_const_words <= quotient_const_words + && candidate_program_words <= quotient_program_words + { + quotient_program_build = Some(candidate); + break; + } + + quotient_const_words = quotient_const_words.max(candidate_const_words); + quotient_program_words = quotient_program_words.max(candidate_program_words); + } + + let quotient_program_build = quotient_program_build.unwrap_or_else(|| { + panic!( + "quotient VK payload reservation did not converge after 8 iterations: const_words={quotient_const_words}, program_words={quotient_program_words}" + ) + }); + let payload_layout = VkPayloadLayout::for_vk( + header_words, + quotient_const_words, + quotient_program_words, + vk.fixed_comms.len(), + vk.permutation_comms.len(), + ) + .unwrap_or_else(|err| panic!("invalid VK payload layout reservation: {err}")); + let quotient_const_offset_words = payload_layout + .word_offset(PayloadSectionKind::QuotientConstants) + .expect("quotient constants section"); + let quotient_program_offset_words = payload_layout + .word_offset(PayloadSectionKind::QuotientProgram) + .expect("quotient program section"); + assert_eq!( + payload_layout + .word_len(PayloadSectionKind::QuotientConstants) + .expect("quotient constants section length"), + quotient_const_words + ); + assert_eq!( + payload_layout + .word_len(PayloadSectionKind::QuotientProgram) + .expect("quotient program section length"), + quotient_program_words + ); + assert_eq!( + payload_layout.total_bytes(), + vk.len(), + "typed VK payload layout must preserve the emitted byte length" + ); + + let quotient_program_chunks = + PackedProgramCodec::encode_words("ient_program_build.bytes); + assert!( + quotient_program_build.consts.len() <= quotient_const_words, + "quotient const table exceeded VK payload reservation" + ); + assert!( + quotient_program_chunks.len() <= quotient_program_words, + "quotient program length exceeded VK payload reservation" + ); + + for (i, value) in quotient_program_build.consts.iter().copied().enumerate() { + vk.constants[quotient_const_offset_words + i] = ("quotient_const", value); + } + for (i, value) in quotient_program_chunks.iter().copied().enumerate() { + vk.constants[quotient_program_offset_words + i] = ("quotient_program", value); + } + + vk.quotient_const_offset_words = Some(quotient_const_offset_words); + vk.quotient_const_words = quotient_const_words; + vk.quotient_program_offset_words = Some(quotient_program_offset_words); + vk.quotient_program_words = quotient_program_words; + vk.validate_payload_layout() + .unwrap_or_else(|err| panic!("invalid generated VK payload layout: {err}")); + vk + } + + /// Build metadata, data handles, and memory layout for a candidate VK base. + fn meta_data_for_vk( + &self, + vk: &Halo2VerifyingKey, + vk_mptr: Ptr, + ) -> (ConstraintSystemMeta, Data, VerifierMemoryLayout) { + // ------------------------------------------------------------------ + // Phase 3 / outer-fewer-point-sets two-pass `Data` construction. + // + // Pass 1: build `Data` against the *raw* meta (no dummy evals). + // This gives us the raw query list whose commitment identity + // structure feeds `compute_dummy_queries`. + // + // Pass 2: bump meta.num_evals by the dummy count (so the + // memory layout - REVERSED_EVALS_MPTR buffer + downstream + // comms_mptr_base - grows to fit the dummies), rebuild Data, + // and populate the dummy eval Words. + // + // When the `outer-fewer-point-sets` Cargo feature is OFF, the dummy + // count is forced to zero; pass 2 collapses to "rebuild Data + // against unchanged meta", and the result is byte-identical + // to the pre-Phase-3 single-pass path. + // ------------------------------------------------------------------ + let raw_memory = self.memory_layout_for( + self.meta, + vk, + vk_mptr, + VerifierMemoryLayoutConfig::default(), + ); + let raw_data = Data::new(self.meta, vk, &raw_memory); + let mut meta = (*self.meta).clone(); + let n_dummy = if cfg!(feature = "outer-fewer-point-sets") { + kzg::num_dummy_queries(&meta, &raw_data) + } else { + 0 + }; + let main_evals = meta.num_evals; + meta.set_num_dummy_evals(n_dummy); + let memory = + self.memory_layout_for(&meta, vk, vk_mptr, VerifierMemoryLayoutConfig::default()); + let mut data = Data::new(&meta, vk, &memory); + if n_dummy > 0 { + data.set_dummy_eval_words(main_evals, n_dummy); + } + + meta.set_num_point_sets(kzg::num_point_sets(&meta, &data)); + let memory = + self.memory_layout_for(&meta, vk, vk_mptr, VerifierMemoryLayoutConfig::default()); + (meta, data, memory) + } + + /// Find a VK memory base that is stable after proof-shape planning. + /// + /// Transcript/PCS low-memory requirements can grow when dummy query + /// planning discovers extra eval scalars. Iterate until the chosen + /// `VK_MPTR` matches the requirements computed from the resulting + /// metadata. + pub(super) fn meta_data_for_stable_static_layout( + &self, + vk: &Halo2VerifyingKey, + ) -> (Ptr, ConstraintSystemMeta, Data, VerifierMemoryLayout) { + let mut vk_mptr = Ptr::memory(self.static_working_memory_size_for_meta(self.meta)); + + for _ in 0..3 { + let (meta, data, memory) = self.meta_data_for_vk(vk, vk_mptr); + let planned_mptr = self.static_working_memory_size_for_meta(&meta); + if planned_mptr == vk_mptr.value().as_usize() { + return (vk_mptr, meta, data, memory); + } + vk_mptr = Ptr::memory(planned_mptr); + } + + panic!("static verifier memory layout did not converge after proof-shape planning"); + } + + /// Build a verifier memory layout after filling derived config fields. + pub(super) fn memory_layout_for( + &self, + meta: &ConstraintSystemMeta, + vk: &Halo2VerifyingKey, + vk_mptr: Ptr, + mut config: VerifierMemoryLayoutConfig, + ) -> VerifierMemoryLayout { + config.transcript_words = + Self::transcript_buffer_layout_for_meta(meta, self.num_instances).words; + config.num_instances = self.num_instances; + VerifierMemoryLayout::new(meta, vk, vk_mptr, config) + } + /// Low-memory working-set size that must stay below `VK_MPTR`. + fn static_working_memory_size_for_meta(&self, meta: &ConstraintSystemMeta) -> usize { + let pcs_computation = kzg::static_working_memory_size(); + let transcript_words = + Self::transcript_buffer_layout_for_meta(meta, self.num_instances).words; + let transcript_end = layout::TRANSCRIPT_BUFFER_START + transcript_words * WORD_BYTES; + let pcs_end = layout::PCS_PAIRING_SCRATCH_START + pcs_computation * WORD_BYTES; + let final_pairing_end = + layout::FINAL_PAIRING_SCRATCH_START + layout::PAIRING_STATIC_WORKING_WORDS * WORD_BYTES; + let verifier_return_end = layout::VERIFIER_RETURN_BUFFER_START + WORD_BYTES; + let quotient_return_end = + layout::QUOTIENT_RETURN_BUFFER_START + (2 + meta.num_simple_selectors) * WORD_BYTES; + + itertools::max([ + // Transcript buffer (streaming Keccak256). The buffer must + // fit *below* `VK_MPTR` because every `mload(VK_MPTR + ...)` + // assumes the VK contract bytes copied via `extcodecopy` + // remain intact, and the buffer would otherwise overwrite + // them as it grows past the start of the VK area. + transcript_end, + // PCS computation scratch + pcs_end, + // Pairing: two-pair input frame plus output word, rooted above + // Solidity's reserved memory prefix. + final_pairing_end, + // Low-memory return frames. The quotient evaluator's output grows + // with the number of simple selector buckets. + verifier_return_end, + quotient_return_end, + ]) + .unwrap() + } + + #[cfg(test)] + /// Test helper exposing the transcript buffer word bound. + pub(crate) fn transcript_buffer_words_bound( + meta: &ConstraintSystemMeta, + num_instances: usize, + ) -> usize { + Self::transcript_buffer_layout_for_meta(meta, num_instances).words + } + + /// Compute the transcript buffer layout for a metadata snapshot. + pub(super) fn transcript_buffer_layout_for_meta( + meta: &ConstraintSystemMeta, + num_instances: usize, + ) -> TranscriptBufferLayout { + // The Step 6 transcript model is a streaming Keccak256 buffer rooted + // at TRANSCRIPT_BUFFER_START. The buffer monotonically grows between + // two challenge squeezes and is reset to one seed word after each + // squeeze. This function returns the number of words needed above + // that root; the caller adds the 0x80 base when choosing VK_MPTR. For + // midnight-proofs verifiers the dominating run is whichever of the + // following is largest: + // (a) initial absorbs (vk_digest + committed_pi + num_instances + // + all instance scalars + all phase-1 advices) before the + // first user-phase challenge squeeze (`theta`), or + // (b) the evaluation block (all `num_evals` scalars) absorbed + // after the `y` squeeze and before the next squeeze. + // + // We compute a per-run conservative upper bound for each and + // take the max, then add the 32-byte post-squeeze seed cushion. + // + // Per-absorb costs in the patched (uncompressed-G1) emitter: + // - word = 32 bytes (`common_word`) + // - uncompressed G1 = 128 bytes (`common_uncompressed_g1`) + // - squeeze output = 32 bytes (post-squeeze seed) + // The earlier (compressed-G1) emitter used 49 bytes per G1; the + // 49 used here is wrong now that the verifier hashes the 128-byte + // EIP-2537 padded form, so we use 128. Mismatching the bound + // causes the keccak buffer to overrun `VK_MPTR` mid-verify and + // silently corrupt `K_MPTR`, `OMEGA_MPTR`, etc., producing a + // multi-billion-gas spin in the Lagrange block. + let proof_layout = ProofCalldataLayout::from_protocol( + &meta.protocol, + 0, + meta.num_evals, + meta.num_point_sets, + ); + TranscriptBufferLayout::from_proof_layout(&proof_layout, num_instances) + } +} diff --git a/proofs/solidity-verifier/src/test.rs b/proofs/solidity-verifier/src/test.rs new file mode 100644 index 000000000..2cdd3e9cd --- /dev/null +++ b/proofs/solidity-verifier/src/test.rs @@ -0,0 +1,2836 @@ +// SPDX-License-Identifier: CC0-1.0 +//! EVM-enabled integration-style tests for rendered Solidity verifier behavior. +//! +//! This module is compiled only for crate tests with the `evm` feature. It +//! exercises compiled verifier contracts, calldata encoding, proof repacking, +//! and runtime failure modes that cannot be covered by pure codegen unit tests. + +#[cfg(feature = "rust-verifier-trace")] +use std::collections::BTreeMap; +use std::{ + env, + panic::AssertUnwindSafe, + path::{Path, PathBuf}, + sync::OnceLock, +}; + +use ff::Field; +use group::{Curve as _, Group as _}; +use midnight_circuits::{ + hash::poseidon::PoseidonChip, + instructions::{hash::HashCPU, AssignmentInstructions, PublicInputInstructions}, +}; +use midnight_curves::{Bls12, Fq, G1Projective, G2Projective}; +use midnight_proofs::{ + circuit::{Layouter, SimpleFloorPlanner, Value}, + plonk::{ + create_proof, keygen_pk, keygen_vk_with_k, prepare, Advice, Circuit, Column, + ConstraintSystem, Constraints, Error, Expression, Fixed, SecondPhase, Selector, + }, + poly::{commitment::Guard as _, kzg::KZGCommitmentScheme, Rotation}, + transcript::{CircuitTranscript, Transcript}, +}; +use midnight_zk_stdlib::{ + setup_vk, utils::plonk_api::srs_for_test, MidnightVK, Relation, ZkStdLib, ZkStdLibArch, +}; +use proptest::{ + prelude::any, + test_runner::{Config as ProptestConfig, TestRunner}, +}; +use rand::SeedableRng; +use rand_chacha::ChaCha8Rng; +#[cfg(feature = "rust-verifier-trace")] +use revm::primitives::B256; +use ruint::aliases::U256; +use sha3::Digest; + +use crate::{ + compile_solidity, encode_calldata, pinned_solc_available, CallOutcome, Evm, GeneratorConfig, + RenderDiagnostics, RenderOptions, RenderQuotient, RenderVk, SolidityGenerator, + FN_SIG_VERIFY_PROOF, +}; + +/// Scalar field used by the BLS12-381 Poseidon fixtures. +type F = midnight_curves::Fq; +/// KZG parameters type used by Poseidon fixture generation. +type PoseidonParams = midnight_proofs::poly::kzg::params::ParamsKZG; +/// Verifier-only KZG parameters type used for native verification checks. +type PoseidonVerifierParams = + midnight_proofs::poly::kzg::params::ParamsVerifierKZG; + +/// Small fixture domain size for Poseidon verifier tests. +const POSEIDON_K: u32 = 6; +/// Environment flag that opts into expensive EVM/Solidity integration tests. +const RUN_EVM_TESTS_ENV: &str = "HALO2_SOLIDITY_RUN_EVM_TESTS"; + +#[test] +fn function_signature() { + assert_eq!( + <[u8; 32]>::from(sha3::Keccak256::digest("verifyProof(bytes,uint256[])"))[..4], + FN_SIG_VERIFY_PROOF, + ); +} + +/// Direct EIP-2537 precompile conformance tests against the bundled +/// Prague-spec revm. Exercises the runner path independently of the verifier +/// codegen so a regression in `src/evm.rs`, malformed-point handling, or the +/// gas path used by large verifier MSMs shows up here first. +#[test] +fn prague_evm_runs_eip2537_conformance_smoke_tests() { + use revm::primitives::Address; + + use crate::evm::test::Evm; + + let mut evm = Evm::default(); + + let g1add = Address::with_last_byte(0x0b); + let g1msm = Address::with_last_byte(0x0c); + let pairing = Address::with_last_byte(0x0f); + let g1 = G1Projective::generator(); + let g2 = G2Projective::generator(); + + // G1ADD(identity, identity) -> identity. + let (_gas_used, output) = evm.call(g1add, vec![0; 0x100]); + assert_eq!(output, vec![0; 0x80]); + + // G1ADD(G, G) -> 2G. + let mut g1add_input = g1_bytes(g1); + g1add_input.extend(g1_bytes(g1)); + let (_gas_used, output) = evm.call(g1add, g1add_input); + assert_eq!(output, g1_bytes(g1 * Fq::from(2))); + + // G1MSM([(identity, 0)]) -> identity. + let (_gas_used, output) = evm.call(g1msm, vec![0; 0xa0]); + assert_eq!(output, vec![0; 0x80]); + + // G1MSM([(G, 3), ([2]G, 5)]) -> [13]G. + let mut g1msm_input = g1_msm_term(g1, 3); + g1msm_input.extend(g1_msm_term(g1 * Fq::from(2), 5)); + let (_gas_used, output) = evm.call(g1msm, g1msm_input); + assert_eq!(output, g1_bytes(g1 * Fq::from(13))); + + // Malformed/off-curve G1 input must be rejected by the MSM precompile. + let mut invalid_msm = eip2537_padded_off_curve_g1_bytes().to_vec(); + invalid_msm.extend(u256_word(1)); + assert!( + !matches!( + evm.try_call_with_gas(g1msm, invalid_msm, 200_000), + CallOutcome::Success { .. } + ), + "G1MSM accepted a canonical but off-curve G1 point" + ); + + // Full-size 78-term MSM path used by wide verifier shapes. The exact gas + // number is fork-dependent; this asserts the local Prague runner has a + // conforming large-input path and returns a non-identity point. + let mut large_msm = Vec::with_capacity(78 * 0xa0); + for _ in 0..78 { + large_msm.extend(g1_msm_term(g1, 1)); + } + match evm.try_call_with_gas(g1msm, large_msm, 2_000_000) { + CallOutcome::Success { output, .. } => { + assert_eq!(output.len(), 0x80); + assert!( + output.iter().any(|&b| b != 0), + "78-term G1MSM unexpectedly returned identity" + ); + } + outcome => panic!("78-term G1MSM failed in Prague revm: {outcome:?}"), + } + + // Pairing identity input should return true. + let (_gas_used, output) = evm.call(pairing, vec![0; 0x180]); + assert_eq!(output, [vec![0; 31], vec![1]].concat()); + + // A single non-identity generator pairing is not the neutral product. + let mut invalid_pairing = g1_bytes(g1); + invalid_pairing.extend(g2_bytes(g2)); + let (_gas_used, output) = evm.call(pairing, invalid_pairing); + assert_eq!( + output, + vec![0; 0x20], + "PAIRING_CHECK([(G1, G2)]) should return false" + ); + + // Bilinearity: e([2]G1, G2) * e(-G1, [2]G2) == 1. + let mut bilinear_pairing = g1_bytes(g1 * Fq::from(2)); + bilinear_pairing.extend(g2_bytes(g2)); + bilinear_pairing.extend(g1_bytes(-g1)); + bilinear_pairing.extend(g2_bytes(g2 * Fq::from(2))); + let (_gas_used, output) = evm.call(pairing, bilinear_pairing); + assert_eq!(output, [vec![0; 31], vec![1]].concat()); +} + +/// Encode a G1 point in the padded EIP-2537 byte layout. +fn g1_bytes(point: G1Projective) -> Vec { + words_to_bytes(crate::__test_only_g1_to_u256s(&point.to_affine())) +} + +/// Encode a G2 point in the padded EIP-2537 byte layout. +fn g2_bytes(point: G2Projective) -> Vec { + words_to_bytes(crate::__test_only_g2_to_u256s(&point.to_affine())) +} + +/// Build one `(G1, scalar)` input tuple for the G1MSM precompile. +fn g1_msm_term(point: G1Projective, scalar: u64) -> Vec { + let mut out = g1_bytes(point); + out.extend(u256_word(scalar)); + out +} + +/// Encode a small integer as one big-endian EVM word. +fn u256_word(value: u64) -> [u8; 32] { + U256::from(value).to_be_bytes() +} + +/// Flatten U256 words into big-endian bytes. +fn words_to_bytes(words: [U256; N]) -> Vec { + words.into_iter().flat_map(|word| word.to_be_bytes::<32>()).collect() +} + +#[derive(Clone, Copy, Debug, Default)] +struct ShapeFuzzSpec { + next_rotation: bool, + second_phase: bool, + permutation: bool, + lookup: bool, + additive_selector: bool, + complex_selector: bool, + fixed_scale: bool, + tag: u64, +} + +#[derive(Clone, Copy, Debug)] +struct ShapeFuzzCase { + name: &'static str, + k: u32, + spec: ShapeFuzzSpec, + seed: u64, +} + +#[derive(Clone, Debug)] +struct ShapeFuzzConfig { + a: Column, + b: Column, + out: Column, + phase2: Option>, + fixed_scale: Option>, + lookup_table: Option>, + selector: Selector, +} + +#[derive(Clone, Debug)] +struct ShapeFuzzCircuit { + spec: ShapeFuzzSpec, + a: F, + b: F, +} + +impl ShapeFuzzCircuit { + /// Deterministically construct a shape-fuzz witness. + fn new(spec: ShapeFuzzSpec, seed: u64) -> Self { + let a = F::from(seed.wrapping_mul(17).wrapping_add(5)); + let b = F::from(seed.wrapping_mul(29).wrapping_add(11)); + Self { spec, a, b } + } + + /// Advice output constrained by the synthetic circuit. + fn out(&self) -> F { + self.a + self.b + F::from(self.spec.tag + 19) + } + + /// Public input derived from the output and advice values. + fn public_instance(&self) -> F { + self.out() - self.a - self.b + } + + /// Next-row advice value used when the shape enables rotations. + fn next_a(&self) -> F { + self.a + F::from(self.spec.tag + 7) + } + + /// Second-phase advice value used when the shape enables phase two. + fn phase2_value(&self) -> F { + self.out() + F::from(self.spec.tag + 23) + } +} + +impl Circuit for ShapeFuzzCircuit { + /// Config columns for the generated shape-fuzz circuit. + type Config = ShapeFuzzConfig; + /// Simple floor planner is enough for these synthetic fixtures. + type FloorPlanner = SimpleFloorPlanner; + /// Shape parameters are supplied through Halo2's parameterized configure. + type Params = ShapeFuzzSpec; + + /// Return the circuit shape without witness values. + fn without_witnesses(&self) -> Self { + Self { + spec: self.spec, + a: F::ZERO, + b: F::ZERO, + } + } + + /// Return the shape parameters used during configuration. + fn params(&self) -> Self::Params { + self.spec + } + + /// The non-parameterized configure path is intentionally disabled. + fn configure(_meta: &mut ConstraintSystem) -> Self::Config { + unreachable!("ShapeFuzzCircuit is always configured with explicit params") + } + + /// Configure a synthetic circuit with optional features toggled by `spec`. + fn configure_with_params(meta: &mut ConstraintSystem, spec: Self::Params) -> Self::Config { + let a = meta.advice_column(); + let b = meta.advice_column(); + let out = meta.advice_column(); + let phase2 = spec.second_phase.then(|| meta.advice_column_in(SecondPhase)); + let fixed_scale = spec.fixed_scale.then(|| meta.fixed_column()); + let lookup_table = spec.lookup.then(|| meta.fixed_column()); + let committed_instance = meta.instance_column(); + let public_instance = meta.instance_column(); + + if spec.permutation { + for column in [a, b, out] { + meta.enable_equality(column); + } + } + + let selector = if spec.additive_selector || spec.complex_selector || spec.lookup { + meta.complex_selector() + } else { + meta.selector() + }; + + meta.create_gate("shape-fuzz arithmetic", |meta| { + let a_cur = meta.query_advice(a, Rotation::cur()); + let b_cur = meta.query_advice(b, Rotation::cur()); + let out_cur = meta.query_advice(out, Rotation::cur()); + let committed = meta.query_instance(committed_instance, Rotation::cur()); + let public = meta.query_instance(public_instance, Rotation::cur()); + + let mut balance = a_cur.clone() + b_cur + public + committed - out_cur.clone(); + if let Some(scale) = fixed_scale { + balance = meta.query_fixed(scale, Rotation::cur()) * balance; + } + + let mut constraints = vec![("public balance", balance)]; + if spec.next_rotation { + constraints.push(( + "next rotation", + meta.query_advice(a, Rotation::next()) + - a_cur + - Expression::Constant(F::from(spec.tag + 7)), + )); + } + if let Some(phase2) = phase2 { + constraints.push(( + "second phase", + meta.query_advice(phase2, Rotation::cur()) + - out_cur + - Expression::Constant(F::from(spec.tag + 23)), + )); + } + + if spec.additive_selector { + Constraints::with_additive_selector(selector, constraints) + } else { + Constraints::with_selector(selector, constraints) + } + }); + + if let Some(table) = lookup_table { + meta.lookup_any("shape-fuzz lookup", Some(selector), |meta| { + vec![( + meta.query_advice(a, Rotation::cur()), + meta.query_fixed(table, Rotation::cur()), + )] + }); + } + + ShapeFuzzConfig { + a, + b, + out, + phase2, + fixed_scale, + lookup_table, + selector, + } + } + + /// Assign witness rows and optional lookup/permutation/phase-two cells. + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + layouter.assign_region( + || "shape-fuzz witness", + |mut region| { + config.selector.enable(&mut region, 0)?; + region.assign_advice(|| "a", config.a, 0, || Value::known(self.a))?; + region.assign_advice(|| "b", config.b, 0, || Value::known(self.b))?; + region.assign_advice(|| "out", config.out, 0, || Value::known(self.out()))?; + + if self.spec.next_rotation { + region.assign_advice( + || "a next", + config.a, + 1, + || Value::known(self.next_a()), + )?; + } + if let Some(column) = config.phase2 { + region.assign_advice( + || "second phase value", + column, + 0, + || Value::known(self.phase2_value()), + )?; + } + if let Some(column) = config.fixed_scale { + region.assign_fixed(|| "fixed scale", column, 0, || Value::known(F::ONE))?; + } + if let Some(column) = config.lookup_table { + region.assign_fixed( + || "lookup table value", + column, + 0, + || Value::known(self.a), + )?; + } + + if self.spec.permutation { + let copied = region.assign_advice( + || "permutation source", + config.a, + 2, + || Value::known(self.b), + )?; + copied.copy_advice(|| "permutation target", &mut region, config.b, 3)?; + } + + Ok(()) + }, + ) + } +} + +#[test] +fn lookup_shape_verifier_compiles_with_native_lookup_callback() { + if !shape_fuzz_inputs_available_for_evm() { + return; + } + + let case = ShapeFuzzCase { + name: "native lookup compile", + k: 5, + seed: 303, + spec: ShapeFuzzSpec { + second_phase: true, + lookup: true, + fixed_scale: true, + tag: 3, + ..ShapeFuzzSpec::default() + }, + }; + let circuit = ShapeFuzzCircuit::new(case.spec, case.seed); + let mut setup_rng = ChaCha8Rng::seed_from_u64(case.seed ^ 0x5eed_5eed); + let params = PoseidonParams::unsafe_setup(case.k, &mut setup_rng); + let vk = keygen_vk_with_k::, _>(¶ms, &circuit, case.k) + .unwrap_or_else(|err| panic!("shape fuzz `{}` vk generation failed: {err:?}", case.name)); + + let generator = SolidityGenerator::new(¶ms, &vk, GeneratorConfig::new(1, 1)); + let artifacts = generator + .render(RenderOptions { + vk: RenderVk::Separate, + ..RenderOptions::default() + }) + .unwrap_or_else(|err| panic!("shape fuzz `{}` render failed: {err:?}", case.name)); + let verifier_solidity = artifacts.verifier; + let vk_solidity = artifacts.verifying_key.expect("separate render includes VK"); + + assert!( + verifier_solidity.contains("case 0x1f"), + "lookup verifier should include the native lookup VM callback" + ); + assert!( + verifier_solidity.contains("q_lookup_f"), + "lookup verifier should include the structured lookup callback body" + ); + + assert!(!compile_solidity(verifier_solidity).is_empty()); + assert!(!compile_solidity(vk_solidity).is_empty()); +} + +#[test] +fn supported_shape_circuit_fuzz_e2e() { + if !shape_fuzz_inputs_available_for_evm() { + return; + } + + let cases = [ + ShapeFuzzCase { + name: "simple-selector current-row fixed-scale", + k: 5, + seed: 101, + spec: ShapeFuzzSpec { + fixed_scale: true, + tag: 1, + ..ShapeFuzzSpec::default() + }, + }, + ShapeFuzzCase { + name: "next-rotation permutation", + k: 5, + seed: 202, + spec: ShapeFuzzSpec { + next_rotation: true, + permutation: true, + tag: 2, + ..ShapeFuzzSpec::default() + }, + }, + ShapeFuzzCase { + name: "second-phase lookup", + k: 5, + seed: 303, + spec: ShapeFuzzSpec { + second_phase: true, + lookup: true, + fixed_scale: true, + tag: 3, + ..ShapeFuzzSpec::default() + }, + }, + ShapeFuzzCase { + name: "trash-additive selector with lookup", + k: 5, + seed: 404, + spec: ShapeFuzzSpec { + next_rotation: true, + lookup: true, + additive_selector: true, + fixed_scale: true, + tag: 4, + ..ShapeFuzzSpec::default() + }, + }, + ]; + + let requested = env::var("SHAPE_FUZZ_CASES") + .ok() + .and_then(|value| value.parse::().ok()) + .unwrap_or(cases.len()) + .min(cases.len()); + + #[cfg(feature = "rust-verifier-trace")] + let mut compared_selector_folds = false; + for case in cases.iter().take(requested) { + let case_compared_selector_folds = run_supported_shape_fuzz_case(case); + #[cfg(feature = "rust-verifier-trace")] + { + compared_selector_folds |= case_compared_selector_folds; + } + #[cfg(not(feature = "rust-verifier-trace"))] + { + let _ = case_compared_selector_folds; + } + } + + #[cfg(feature = "rust-verifier-trace")] + if requested > 0 { + assert!( + compared_selector_folds, + "shape fuzz trace suite should include at least one selector-fold comparison" + ); + } +} + +/// Render, deploy, and exercise one supported shape-fuzz case end to end. +fn run_supported_shape_fuzz_case(case: &ShapeFuzzCase) -> bool { + let circuit = ShapeFuzzCircuit::new(case.spec, case.seed); + let mut setup_rng = ChaCha8Rng::seed_from_u64(case.seed ^ 0x5eed_5eed); + let params = PoseidonParams::unsafe_setup(case.k, &mut setup_rng); + let vk = keygen_vk_with_k::, _>(¶ms, &circuit, case.k) + .unwrap_or_else(|err| panic!("shape fuzz `{}` vk generation failed: {err:?}", case.name)); + let pk = keygen_pk(vk, &circuit) + .unwrap_or_else(|err| panic!("shape fuzz `{}` pk generation failed: {err:?}", case.name)); + + let committed = [F::ZERO]; + let public = [circuit.public_instance()]; + let all_instance_columns: [&[F]; 2] = [&committed, &public]; + let mut proof_rng = ChaCha8Rng::seed_from_u64(case.seed ^ 0x0bad_f00d); + let mut transcript = CircuitTranscript::::init(); + create_proof::, _, _>( + ¶ms, + &pk, + std::slice::from_ref(&circuit), + 1, + &[&all_instance_columns], + &mut proof_rng, + &mut transcript, + ) + .unwrap_or_else(|err| { + panic!( + "shape fuzz `{}` proof generation failed: {err:?}", + case.name + ) + }); + let compressed_proof = transcript.finalize(); + + let committed_pi = [G1Projective::identity()]; + let public_columns: [&[F]; 1] = [&public]; + let mut transcript = CircuitTranscript::::init_from_bytes(&compressed_proof); + let guard = prepare::, CircuitTranscript>( + pk.get_vk(), + &[&committed_pi], + &[&public_columns], + &mut transcript, + ) + .unwrap_or_else(|err| panic!("shape fuzz `{}` native prepare failed: {err:?}", case.name)); + transcript.assert_empty().unwrap_or_else(|_| { + panic!( + "shape fuzz `{}` native transcript had trailing bytes", + case.name + ) + }); + guard + .verify(¶ms.verifier_params()) + .unwrap_or_else(|err| panic!("shape fuzz `{}` native verify failed: {err:?}", case.name)); + + let generator = + SolidityGenerator::new(¶ms, pk.get_vk(), GeneratorConfig::new(public.len(), 1)); + let artifacts = generator + .render(RenderOptions { + vk: RenderVk::Separate, + ..RenderOptions::default() + }) + .unwrap_or_else(|err| panic!("shape fuzz `{}` render failed: {err:?}", case.name)); + let verifier_solidity = artifacts.verifier; + let vk_solidity = artifacts.verifying_key.expect("separate render includes VK"); + let repacked_proof = generator + .repack_proof(&compressed_proof) + .unwrap_or_else(|err| panic!("shape fuzz `{}` repack failed: {err:?}", case.name)); + let mut deployed = deploy_separate_verifier_from_sources(&verifier_solidity, &vk_solidity); + + assert_solidity_accepts( + call_deployed_verifier(&mut deployed, &repacked_proof, &public), + &format!("shape fuzz `{}` valid proof", case.name), + ); + + #[cfg(feature = "rust-verifier-trace")] + let compared_selector_folds = assert_shape_fuzz_trace_matches_native_midfall( + case, + ¶ms, + pk.get_vk(), + &generator, + &compressed_proof, + &repacked_proof, + &public, + ); + #[cfg(not(feature = "rust-verifier-trace"))] + let compared_selector_folds = false; + + let mut wrong_public = public.to_vec(); + wrong_public[0] += F::ONE; + assert_solidity_rejects( + call_deployed_verifier(&mut deployed, &repacked_proof, &wrong_public), + &format!("shape fuzz `{}` wrong public input", case.name), + ); + + let mut bad_proof = repacked_proof.clone(); + let mutation_idx = bad_proof.len() / 2; + bad_proof[mutation_idx] ^= 0x01; + assert_solidity_rejects( + call_deployed_verifier(&mut deployed, &bad_proof, &public), + &format!( + "shape fuzz `{}` mutated proof byte {mutation_idx}", + case.name + ), + ); + + compared_selector_folds +} + +/// Return whether expensive shape-fuzz EVM tests have their host prerequisites. +fn shape_fuzz_inputs_available_for_evm() -> bool { + if !env_flag_enabled(RUN_EVM_TESTS_ENV) { + eprintln!("skipping supported-shape circuit fuzz: set {RUN_EVM_TESTS_ENV}=1 to run it"); + return false; + } + if !solc_available() { + eprintln!("skipping supported-shape circuit fuzz: solc not found"); + return false; + } + true +} + +#[cfg(feature = "rust-verifier-trace")] +fn assert_shape_fuzz_trace_matches_native_midfall( + case: &ShapeFuzzCase, + params: &PoseidonParams, + vk: &midnight_proofs::plonk::VerifyingKey>, + generator: &SolidityGenerator<'_>, + compressed_proof: &[u8], + repacked_proof: &[u8], + public: &[F], +) -> bool { + use midnight_proofs::plonk::solidity_trace; + + solidity_trace::start(); + let committed_pi = [G1Projective::identity()]; + let public_columns: [&[F]; 1] = [public]; + let mut transcript = CircuitTranscript::::init_from_bytes(compressed_proof); + let guard = prepare::, CircuitTranscript>( + vk, + &[&committed_pi], + &[&public_columns], + &mut transcript, + ) + .unwrap_or_else(|err| { + panic!( + "shape fuzz `{}` native trace prepare failed: {err:?}", + case.name + ) + }); + transcript.assert_empty().unwrap_or_else(|_| { + panic!( + "shape fuzz `{}` native trace transcript had trailing bytes", + case.name + ) + }); + guard.verify(¶ms.verifier_params()).unwrap_or_else(|err| { + panic!( + "shape fuzz `{}` native trace guard failed: {err:?}", + case.name + ) + }); + let rust_trace = solidity_trace::take(); + + let trace_artifacts = generator + .render(RenderOptions { + vk: RenderVk::Separate, + diagnostics: RenderDiagnostics { + trace: true, + ..RenderDiagnostics::default() + }, + ..RenderOptions::default() + }) + .unwrap_or_else(|err| panic!("shape fuzz `{}` trace render failed: {err:?}", case.name)); + let trace_verifier_solidity = trace_artifacts.verifier; + let trace_vk_solidity = + trace_artifacts.verifying_key.expect("trace separate render includes VK"); + let mut deployed = + deploy_separate_verifier_from_sources(&trace_verifier_solidity, &trace_vk_solidity); + let (_gas, output, logs) = deployed.evm.call_with_logs( + deployed.verifier_address, + encode_calldata(repacked_proof, public), + ); + let expected_true = [vec![0u8; 31], vec![1]].concat(); + assert_eq!( + output, expected_true, + "shape fuzz `{}` trace verifier should accept the proof", + case.name + ); + + let mut rust_by_id = BTreeMap::new(); + for event in rust_trace { + assert!( + rust_by_id.insert(event.id, (event.name, event.data)).is_none(), + "shape fuzz `{}` duplicate Rust trace id {}", + case.name, + event.id + ); + } + let solidity_trace = parse_solidity_trace_logs(&logs); + let has_selector_folds = rust_by_id + .keys() + .chain(solidity_trace.keys()) + .any(|id| (60_000..61_000).contains(id)); + assert_trace_equivalence_and_required_coverage( + case.name, + &rust_by_id, + &solidity_trace, + has_selector_folds, + ); + has_selector_folds +} + +#[cfg(feature = "rust-verifier-trace")] +fn assert_trace_equivalence_and_required_coverage( + context: &str, + rust_trace: &BTreeMap)>, + solidity_trace: &BTreeMap>, + require_selector_folds: bool, +) { + let missing = rust_trace + .keys() + .filter(|id| !solidity_trace.contains_key(id)) + .copied() + .collect::>(); + assert!( + missing.is_empty(), + "{context}: Solidity trace is missing native Rust trace ids {missing:?}" + ); + + let unexpected = solidity_trace + .keys() + .filter(|id| !rust_trace.contains_key(id)) + .copied() + .collect::>(); + assert!( + unexpected.is_empty(), + "{context}: Solidity trace emitted ids without a native Rust oracle {unexpected:?}" + ); + + assert_required_diff_trace_coverage_with_options( + rust_trace, + solidity_trace, + require_selector_folds, + ); + + for (id, solidity_data) in solidity_trace { + let (name, rust_data) = + rust_trace.get(id).expect("missing Solidity trace ids were checked above"); + assert_eq!( + rust_data, + solidity_data, + "{context}: trace mismatch id={id} name={name}: rust=0x{} solidity=0x{}", + hex::encode(rust_data), + hex::encode(solidity_data), + ); + } +} + +#[cfg(not(feature = "rust-verifier-trace"))] +#[test] +fn evm_gate_requires_native_solidity_trace_feature() { + if env_flag_enabled(RUN_EVM_TESTS_ENV) { + panic!( + "{RUN_EVM_TESTS_ENV}=1 now requires the `rust-verifier-trace` feature so native/Solidity trace equivalence is part of the EVM gate" + ); + } +} + +#[test] +fn pbt_solidity_verifies_standard_plonk_embedded_vk_proofs() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let mut runner = new_property_test_runner(); + runner + .run(&any::(), |seed| { + run_property_poseidon_positive_case(false, seed); + Ok(()) + }) + .unwrap(); +} + +#[test] +fn pbt_solidity_rejects_wrong_instances() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let mut runner = new_property_test_runner(); + let strategy = (any::(), any::()); + runner + .run(&strategy, |(seed, separate)| { + run_property_poseidon_wrong_instance_case(separate, seed); + Ok(()) + }) + .unwrap(); +} + +#[test] +fn pbt_solidity_rejects_malleated_proofs() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let mut runner = new_property_test_runner(); + let strategy = (any::(), any::(), 0usize..4096); + runner + .run(&strategy, |(seed, separate, bit_idx)| { + run_property_poseidon_malleated_proof_case(separate, seed, bit_idx); + Ok(()) + }) + .unwrap(); +} + +#[test] +fn pbt_solidity_rejects_wrong_verifying_keys() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let mut runner = new_property_test_runner(); + runner + .run(&any::(), |seed| { + run_property_poseidon_wrong_vk_case(seed); + Ok(()) + }) + .unwrap(); +} + +#[test] +fn pbt_separate_vk_digest_prefix_affects_verification() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let mut runner = new_property_test_runner(); + runner + .run(&any::(), |seed| { + run_separate_vk_digest_prefix_affects_verification_case(seed); + Ok(()) + }) + .unwrap(); +} + +#[test] +fn malformed_embedded_calldata_variants_are_rejected() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture = create_property_poseidon_fixture(); + let valid = encode_calldata(&fixture.proof, &fixture.instances); + let valid_true = call_embedded_verifier_raw(&fixture.embedded_verifier_solidity, valid.clone()); + assert_solidity_accepts(valid_true, "valid embedded calldata"); + + let mut wrong_selector = valid.clone(); + wrong_selector[0] ^= 0x01; + + let empty_proof = encode_calldata(&[], &fixture.instances); + let truncated_proof = valid[..valid.len() - 1].to_vec(); + + let mut extra_trailing_bytes = valid.clone(); + extra_trailing_bytes.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]); + + let proof_len = fixture.proof.len(); + let instances_len_word_start = 4 + 0x40 + 0x20 + proof_len; + let mut wrong_instance_array_length = valid.clone(); + overwrite_u256_word( + &mut wrong_instance_array_length, + instances_len_word_start, + fixture.instances.len() as u64 + 1, + ); + for (name, calldata) in [ + ("empty proof", empty_proof), + ("truncated proof", truncated_proof), + ("extra trailing bytes", extra_trailing_bytes), + ("wrong selector", wrong_selector), + ("wrong instance array length", wrong_instance_array_length), + ] { + let output = call_embedded_verifier_raw(&fixture.embedded_verifier_solidity, calldata); + assert_solidity_rejects(output, name); + } +} + +#[test] +fn mutated_separate_vk_contract_is_rejected() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture = create_property_poseidon_fixture(); + let mut baseline = deployed_separate_verifier(&fixture); + assert_deployed_call_accepts( + &mut baseline, + &fixture, + &fixture.proof, + "valid proof before mutated separate vk", + ); + + let mutated_vk_solidity = mutate_first_large_hex_literal(&fixture.vk_solidity, 0); + assert_separate_verifier_rejects_vk_mutation( + &fixture.separate_verifier_solidity, + &mutated_vk_solidity, + &fixture.proof, + &fixture.instances, + "mutated separate vk", + ); +} + +#[test] +fn vk_payload_section_mutations_are_rejected() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture = create_poseidon_vk_sources_fixture(); + let sections = [ + ("header", "vk_digest"), + ("quotient constants", "quotient_const"), + ("quotient program", "quotient_program"), + ("fixed commitments", "fixed_comms[0].x_hi"), + ("permutation commitments", "permutation_comms[0].x_hi"), + ]; + + for (section, marker) in sections { + assert!( + fixture.vk_solidity.contains(marker), + "fixture VK source missing {section} marker `{marker}`" + ); + let mutated_vk_solidity = + mutate_value_hex_literal_on_line_containing(&fixture.vk_solidity, marker); + assert_separate_verifier_rejects_vk_dependency( + &fixture.separate_verifier_solidity, + &mutated_vk_solidity, + &format!("mutated VK payload section: {section}"), + ); + } +} + +#[test] +fn pinned_quotient_verifier_rejects_wrong_vk_and_quotient_contracts() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture = create_property_poseidon_fixture(); + let wrong_vk_solidity = mutate_first_large_hex_literal(&fixture.vk_solidity, 0); + let wrong_quotient_solidity = + mutate_first_large_hex_literal(&fixture.quotient_evaluator_solidity, 0); + + assert_pinned_quotient_constructor_rejects( + &fixture.quotient_verifier_solidity, + &wrong_vk_solidity, + &fixture.quotient_evaluator_solidity, + "wrong VK runtime hash", + ); + assert_pinned_quotient_constructor_rejects( + &fixture.quotient_verifier_solidity, + &fixture.vk_solidity, + &wrong_quotient_solidity, + "wrong quotient runtime hash", + ); + + let verifier_creation_code = compile_solidity(&fixture.quotient_verifier_solidity); + let vk_creation_code = compile_solidity(&fixture.vk_solidity); + let quotient_creation_code = compile_solidity(&fixture.quotient_evaluator_solidity); + assert_pinned_quotient_constructor_rejects_address_args( + &verifier_creation_code, + "ient_creation_code, + "ient_creation_code, + "quotient evaluator supplied as VK", + ); + assert_pinned_quotient_constructor_rejects_address_args( + &verifier_creation_code, + &vk_creation_code, + &vk_creation_code, + "VK supplied as quotient evaluator", + ); +} + +#[test] +fn verifier_constructor_rejects_missing_or_mismatched_eip2537_precompiles() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture = create_property_poseidon_fixture(); + for (name, needle, replacement) in [ + ( + "missing G1ADD precompile", + "staticcall(50000, 0x0b", + "staticcall(50000, 0x12", + ), + ( + "G1MSM routed to G1ADD", + "staticcall(60000, 0x0c", + "staticcall(60000, 0x0b", + ), + ( + "pairing routed to G1MSM", + "staticcall(120000, 0x0f", + "staticcall(120000, 0x0c", + ), + ] { + let verifier_solidity = replace_required_precompile_staticcall( + &fixture.quotient_verifier_solidity, + needle, + replacement, + ); + assert_pinned_quotient_constructor_rejects( + &verifier_solidity, + &fixture.vk_solidity, + &fixture.quotient_evaluator_solidity, + name, + ); + } +} + +#[test] +fn production_renders_do_not_emit_gas_checkpoints() { + if crate::SOLIDITY_GAS_CHECKPOINTS_ENABLED { + return; + } + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture = create_property_poseidon_fixture(); + for (name, source) in [ + ("embedded", fixture.embedded_verifier_solidity.as_str()), + ("separate", fixture.separate_verifier_solidity.as_str()), + ( + "quotient-separated", + fixture.quotient_verifier_solidity.as_str(), + ), + ] { + assert!( + !source.contains("function gas_checkpoint"), + "{name} production render unexpectedly defines gas_checkpoint" + ); + assert!( + !source.contains("log1("), + "{name} production render unexpectedly emits LOG1" + ); + assert!( + source.contains(") external view returns (bool)"), + "{name} production render should keep verifyProof external view" + ); + } +} + +#[test] +fn standard_plonk_render_is_deterministic_for_same_seed() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture_a = load_property_poseidon_fixture(); + let fixture_b = load_property_poseidon_fixture(); + + assert_eq!(fixture_a.instances, fixture_b.instances); + assert_eq!(fixture_a.proof.len(), fixture_b.proof.len()); + assert_eq!( + fixture_a.embedded_verifier_solidity, + fixture_b.embedded_verifier_solidity + ); + assert_eq!( + fixture_a.separate_verifier_solidity, + fixture_b.separate_verifier_solidity + ); + assert_eq!(fixture_a.vk_solidity, fixture_b.vk_solidity); +} + +#[test] +fn compile_solidity_is_deterministic_for_same_source() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture = create_property_poseidon_fixture(); + let bytecode_a = compile_solidity(&fixture.embedded_verifier_solidity); + let bytecode_b = compile_solidity(&fixture.embedded_verifier_solidity); + + assert_eq!(bytecode_a, bytecode_b); +} + +#[test] +fn poseidon_verifier_variants_compile_with_pinned_solc() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture = create_property_poseidon_fixture(); + let variants = [ + ( + "embedded verifier", + fixture.embedded_verifier_solidity.as_str(), + ), + ( + "embedded trace verifier", + fixture.embedded_trace_verifier_solidity.as_str(), + ), + ( + "embedded gas verifier", + fixture.embedded_gas_verifier_solidity.as_str(), + ), + ( + "separate verifier", + fixture.separate_verifier_solidity.as_str(), + ), + ( + "separate gas verifier", + fixture.gas_separate_verifier_solidity.as_str(), + ), + ( + "separate trace verifier", + fixture.trace_verifier_solidity.as_str(), + ), + ("separate VK", fixture.vk_solidity.as_str()), + ("separate trace VK", fixture.trace_vk_solidity.as_str()), + ( + "external quotient verifier", + fixture.quotient_verifier_solidity.as_str(), + ), + ( + "external quotient evaluator", + fixture.quotient_evaluator_solidity.as_str(), + ), + ( + "external quotient trace verifier", + fixture.trace_quotient_verifier_solidity.as_str(), + ), + ( + "external quotient trace evaluator", + fixture.trace_quotient_evaluator_solidity.as_str(), + ), + ( + "external quotient trace VK", + fixture.trace_quotient_vk_solidity.as_str(), + ), + ]; + + for (name, source) in variants { + let bytecode = std::panic::catch_unwind(AssertUnwindSafe(|| compile_solidity(source))) + .unwrap_or_else(|_| panic!("{name} did not compile")); + assert!(!bytecode.is_empty(), "{name} compiled to empty bytecode"); + } +} + +#[cfg(feature = "rust-verifier-trace")] +#[test] +fn native_midfall_verifier_trace_matches_solidity_trace() { + use group::Group; + use midnight_curves::{Bls12, G1Projective}; + use midnight_proofs::{ + plonk::{prepare, solidity_trace}, + poly::{commitment::Guard, kzg::KZGCommitmentScheme}, + transcript::{CircuitTranscript, Transcript}, + }; + + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture = create_property_poseidon_fixture(); + + solidity_trace::start(); + let mut transcript = + CircuitTranscript::::init_from_bytes(&fixture.compressed_proof); + let committed_pi = vec![G1Projective::identity()]; + let public_columns: [&[F]; 1] = [&fixture.instances]; + let guard = prepare::, CircuitTranscript>( + fixture.vk.vk(), + &[&committed_pi], + &[&public_columns], + &mut transcript, + ) + .expect("native prepare succeeds"); + transcript.assert_empty().expect("native transcript consumes proof"); + guard.verify(&fixture.params_verifier).expect("native guard verifies"); + let rust_trace = solidity_trace::take(); + + let mut evm = Evm::default(); + let vk_address = evm.create(compile_solidity(&fixture.trace_vk_solidity)); + let verifier_address = evm.create_with_address_arg( + compile_solidity(&fixture.trace_verifier_solidity), + vk_address, + ); + let (_gas, output, logs) = evm.call_with_logs( + verifier_address, + encode_calldata(&fixture.proof, &fixture.instances), + ); + let solidity_returned_success = output == [vec![0; 31], vec![1]].concat(); + let solidity_trace = parse_solidity_trace_logs(&logs); + + let mut rust_by_id = BTreeMap::new(); + for event in rust_trace { + assert!( + rust_by_id.insert(event.id, (event.name, event.data)).is_none(), + "duplicate Rust trace id {}", + event.id + ); + } + assert_required_diff_trace_coverage(&rust_by_id, &solidity_trace); + + assert_eq!( + rust_by_id.keys().copied().collect::>(), + solidity_trace.keys().copied().collect::>(), + "Rust/Solidity trace ID sets differ" + ); + + for (id, solidity_data) in solidity_trace { + let (name, rust_data) = rust_by_id.get(&id).expect("Rust trace id present"); + assert_eq!( + rust_data, + &solidity_data, + "trace mismatch id={id} name={name}: rust=0x{} solidity=0x{}", + hex::encode(rust_data), + hex::encode(&solidity_data), + ); + } + + assert!( + solidity_returned_success, + "trace verifier returned failure after matching Rust trace" + ); +} + +#[test] +fn trace_verifiers_revert_on_final_pairing_failure() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture = create_property_poseidon_fixture(); + let mut bad_instances = fixture.instances.clone(); + bad_instances[0] += F::ONE; + + assert_solidity_rejects( + call_embedded_verifier( + &fixture.embedded_trace_verifier_solidity, + &fixture.proof, + &bad_instances, + ), + "embedded trace verifier wrong instance", + ); + assert_solidity_rejects( + call_separate_verifier( + &fixture.trace_verifier_solidity, + &fixture.trace_vk_solidity, + &fixture.proof, + &bad_instances, + ), + "separate trace verifier wrong instance", + ); +} + +#[cfg(feature = "rust-verifier-trace")] +fn parse_solidity_trace_logs(logs: &[revm::primitives::Log]) -> BTreeMap> { + let mut trace = BTreeMap::new(); + + for log in logs { + let topics = log.data.topics(); + assert_eq!(topics.len(), 1, "trace log must have one topic"); + + let data = log.data.data.as_ref().to_vec(); + if data.is_empty() { + continue; + } + + let id = trace_topic_id(topics[0]); + assert!( + trace.insert(id, data).is_none(), + "duplicate Solidity trace id {id}" + ); + } + + trace +} + +#[cfg(feature = "rust-verifier-trace")] +fn trace_topic_id(topic: B256) -> u64 { + let bytes = topic.as_slice(); + u64::from_be_bytes(bytes[24..32].try_into().expect("topic is 32 bytes")) +} + +#[cfg(feature = "rust-verifier-trace")] +fn assert_required_diff_trace_coverage( + rust_trace: &BTreeMap)>, + solidity_trace: &BTreeMap>, +) { + assert_required_diff_trace_coverage_with_options(rust_trace, solidity_trace, true); +} + +#[cfg(feature = "rust-verifier-trace")] +fn assert_required_diff_trace_coverage_with_options( + rust_trace: &BTreeMap)>, + solidity_trace: &BTreeMap>, + require_selector_folds: bool, +) { + for (name, id) in [ + ("theta challenge", 7), + ("beta challenge", 8), + ("gamma challenge", 9), + ("y challenge", 10), + ("x challenge", 11), + ("x1 challenge", 13), + ("x2 challenge", 14), + ("x3 challenge", 15), + ("x4 challenge", 16), + ("quotient numerator", 36), + ("f_eval", 31), + ("final MSM commitment", 33), + ("pairing lhs input", 27), + ("pairing rhs input", 28), + ("final pairing result", 35), + ] { + assert_trace_id_present(rust_trace, solidity_trace, id, name); + } + + assert_trace_range_present( + rust_trace, + solidity_trace, + 30_000..40_000, + "quotient identity evaluations", + ); + assert_trace_range_present( + rust_trace, + solidity_trace, + 40_000..41_000, + "PCS q_com point-set commitments", + ); + assert_trace_range_present( + rust_trace, + solidity_trace, + 41_000..42_000, + "serialized PCS point sets", + ); + if require_selector_folds { + assert_trace_range_present(rust_trace, solidity_trace, 60_000..61_000, "selector folds"); + } +} + +#[cfg(feature = "rust-verifier-trace")] +fn assert_trace_id_present( + rust_trace: &BTreeMap)>, + solidity_trace: &BTreeMap>, + id: u64, + name: &str, +) { + assert!( + rust_trace.contains_key(&id), + "Rust trace missing required {name} id {id}" + ); + assert!( + solidity_trace.contains_key(&id), + "Solidity trace missing required {name} id {id}" + ); +} + +#[cfg(feature = "rust-verifier-trace")] +fn assert_trace_range_present( + rust_trace: &BTreeMap)>, + solidity_trace: &BTreeMap>, + range: std::ops::Range, + name: &str, +) { + assert!( + rust_trace.keys().any(|id| range.contains(id)), + "Rust trace missing required {name} in id range {range:?}" + ); + assert!( + solidity_trace.keys().any(|id| range.contains(id)), + "Solidity trace missing required {name} in id range {range:?}" + ); +} + +#[derive(Clone, Debug)] +struct PoseidonExample; + +impl Relation for PoseidonExample { + /// Public output field element for the Poseidon relation. + type Instance = F; + + /// Three-field-element Poseidon preimage. + type Witness = [F; 3]; + + /// Format the Poseidon output as one public instance. + fn format_instance(instance: &Self::Instance) -> Result, Error> { + Ok(vec![*instance]) + } + + /// Build the Poseidon circuit using the zk-stdlib helper chips. + fn circuit( + &self, + std_lib: &ZkStdLib, + layouter: &mut impl Layouter, + _instance: Value, + witness: Value, + ) -> Result<(), Error> { + let assigned_message = std_lib.assign_many(layouter, &witness.transpose_array())?; + let output = std_lib.poseidon(layouter, &assigned_message)?; + std_lib.constrain_as_public_input(layouter, &output) + } + + /// Declare the Poseidon chip dependency for setup helpers. + fn used_chips(&self) -> ZkStdLibArch { + ZkStdLibArch { + poseidon: true, + ..ZkStdLibArch::default() + } + } + + /// Relation serialization is unused for this in-memory fixture. + fn write_relation(&self, _writer: &mut W) -> std::io::Result<()> { + Ok(()) + } + + /// Relation deserialization returns the stateless fixture. + fn read_relation(_reader: &mut R) -> std::io::Result { + Ok(PoseidonExample) + } +} + +#[derive(Clone, Debug)] +struct PropertyPoseidonFixture { + compressed_proof: Vec, + proof: Vec, + scalar_layout: crate::lowering::RepackedProofScalarLayout, + instances: Vec, + params_verifier: PoseidonVerifierParams, + vk: MidnightVK, + embedded_verifier_solidity: String, + embedded_trace_verifier_solidity: String, + embedded_gas_verifier_solidity: String, + separate_verifier_solidity: String, + gas_separate_verifier_solidity: String, + vk_solidity: String, + quotient_verifier_solidity: String, + quotient_evaluator_solidity: String, + trace_quotient_evaluator_solidity: String, + trace_quotient_verifier_solidity: String, + trace_quotient_vk_solidity: String, + #[allow(dead_code)] + trace_verifier_solidity: String, + #[allow(dead_code)] + trace_vk_solidity: String, +} + +#[derive(Clone)] +struct PoseidonVkSourcesFixture { + separate_verifier_solidity: String, + vk_solidity: String, +} + +/// Cached separate-VK Solidity sources for constructor/pinning tests. +fn create_poseidon_vk_sources_fixture() -> PoseidonVkSourcesFixture { + static FIXTURE: OnceLock = OnceLock::new(); + FIXTURE.get_or_init(load_poseidon_vk_sources_fixture).clone() +} + +/// Build the separate verifier and VK source fixture. +fn load_poseidon_vk_sources_fixture() -> PoseidonVkSourcesFixture { + let srs_dir = srs_dir(); + env::set_var("SRS_DIR", &srs_dir); + + let relation = PoseidonExample; + let srs = srs_for_test(&relation, Some(POSEIDON_K)); + let vk = setup_vk(&srs, &relation); + assert_eq!(vk.k() as u32, POSEIDON_K, "unexpected Poseidon VK k"); + + let generator = SolidityGenerator::new(&srs, vk.vk(), GeneratorConfig::new(1, 1)); + let artifacts = generator + .render(RenderOptions { + vk: RenderVk::Separate, + ..RenderOptions::default() + }) + .expect("separate render"); + let separate_verifier_solidity = artifacts.verifier; + let vk_solidity = artifacts.verifying_key.expect("separate render includes VK"); + + PoseidonVkSourcesFixture { + separate_verifier_solidity, + vk_solidity, + } +} + +/// Cached full Poseidon fixture used by property-style tests. +fn create_property_poseidon_fixture() -> PropertyPoseidonFixture { + static FIXTURE: OnceLock = OnceLock::new(); + FIXTURE.get_or_init(load_property_poseidon_fixture).clone() +} + +/// Generate proofs, render verifier variants, and cache host-side layouts. +fn load_property_poseidon_fixture() -> PropertyPoseidonFixture { + let srs_dir = srs_dir(); + env::set_var("SRS_DIR", &srs_dir); + + let relation = PoseidonExample; + let srs = srs_for_test(&relation, Some(POSEIDON_K)); + let vk = setup_vk(&srs, &relation); + assert_eq!(vk.k() as u32, POSEIDON_K, "unexpected Poseidon VK k"); + + let (compressed_proof, instance) = generate_poseidon_proof(&srs, &relation, &vk); + + let generator = SolidityGenerator::new(&srs, vk.vk(), GeneratorConfig::new(1, 1)); + let embedded_verifier_solidity = + generator.render(RenderOptions::default()).expect("embedded render").verifier; + let embedded_trace_verifier_solidity = generator + .render(RenderOptions { + diagnostics: RenderDiagnostics { + trace: true, + ..RenderDiagnostics::default() + }, + ..RenderOptions::default() + }) + .expect("embedded trace render") + .verifier; + let embedded_gas_verifier_solidity = generator + .render(RenderOptions { + diagnostics: RenderDiagnostics { + gas_checkpoints: true, + ..RenderDiagnostics::default() + }, + ..RenderOptions::default() + }) + .expect("embedded gas render") + .verifier; + let separate_artifacts = generator + .render(RenderOptions { + vk: RenderVk::Separate, + ..RenderOptions::default() + }) + .expect("separate render"); + let separate_verifier_solidity = separate_artifacts.verifier; + let vk_solidity = separate_artifacts.verifying_key.expect("separate render includes VK"); + let gas_separate_artifacts = generator + .render(RenderOptions { + vk: RenderVk::Separate, + diagnostics: RenderDiagnostics { + gas_checkpoints: true, + ..RenderDiagnostics::default() + }, + ..RenderOptions::default() + }) + .expect("separate gas render"); + let gas_separate_verifier_solidity = gas_separate_artifacts.verifier; + let gas_vk_solidity = + gas_separate_artifacts.verifying_key.expect("separate gas render includes VK"); + assert_eq!( + vk_solidity, gas_vk_solidity, + "plain and gas-checkpoint render paths must share the same VK" + ); + let quotient_evaluator_solidity = generator + .render_quotient_evaluator(RenderDiagnostics::default()) + .expect("quotient evaluator render"); + let quotient_creation_code = compile_solidity("ient_evaluator_solidity); + let mut pin_evm = Evm::default(); + let quotient_address = pin_evm.create(quotient_creation_code); + let quotient_runtime_size = pin_evm.code_size(quotient_address); + let quotient_codehash = pin_evm.code_hash(quotient_address); + let trace_quotient_evaluator_solidity = generator + .render_quotient_evaluator(RenderDiagnostics { + trace: true, + ..RenderDiagnostics::default() + }) + .expect("trace quotient evaluator render"); + let trace_quotient_creation_code = compile_solidity(&trace_quotient_evaluator_solidity); + let mut trace_pin_evm = Evm::default(); + let trace_quotient_address = trace_pin_evm.create(trace_quotient_creation_code); + let trace_quotient_runtime_size = trace_pin_evm.code_size(trace_quotient_address); + let trace_quotient_codehash = trace_pin_evm.code_hash(trace_quotient_address); + let quotient_artifacts = generator + .render(RenderOptions { + vk: RenderVk::Separate, + quotient: RenderQuotient::ExternalPinned { + runtime_len: quotient_runtime_size, + codehash: quotient_codehash, + }, + ..RenderOptions::default() + }) + .expect("separate pinned render with quotient evaluator"); + let quotient_verifier_solidity = quotient_artifacts.verifier; + let quotient_vk_solidity = + quotient_artifacts.verifying_key.expect("pinned separate render includes VK"); + let pinned_quotient_solidity = quotient_artifacts + .quotient_evaluator + .expect("pinned render includes quotient evaluator"); + let trace_quotient_artifacts = generator + .render(RenderOptions { + vk: RenderVk::Separate, + quotient: RenderQuotient::ExternalPinned { + runtime_len: trace_quotient_runtime_size, + codehash: trace_quotient_codehash, + }, + diagnostics: RenderDiagnostics { + trace: true, + ..RenderDiagnostics::default() + }, + }) + .expect("trace pinned render with quotient evaluator"); + let trace_quotient_verifier_solidity = trace_quotient_artifacts.verifier; + let trace_quotient_vk_solidity = trace_quotient_artifacts + .verifying_key + .expect("trace pinned separate render includes VK"); + let trace_pinned_quotient = trace_quotient_artifacts + .quotient_evaluator + .expect("trace pinned render includes quotient evaluator"); + assert_eq!( + quotient_evaluator_solidity, pinned_quotient_solidity, + "pinning the quotient evaluator must not change the evaluator source" + ); + assert_eq!( + trace_quotient_evaluator_solidity, trace_pinned_quotient, + "trace pinning must not change the trace quotient evaluator source" + ); + assert_eq!( + vk_solidity, quotient_vk_solidity, + "plain and quotient-separated render paths must share the same VK" + ); + assert_eq!( + vk_solidity, trace_quotient_vk_solidity, + "plain and trace quotient-separated render paths must share the same VK" + ); + let trace_artifacts = generator + .render(RenderOptions { + vk: RenderVk::Separate, + diagnostics: RenderDiagnostics { + trace: true, + ..RenderDiagnostics::default() + }, + ..RenderOptions::default() + }) + .expect("trace render"); + let trace_verifier_solidity = trace_artifacts.verifier; + let trace_vk_solidity = + trace_artifacts.verifying_key.expect("trace separate render includes VK"); + let proof = generator.repack_proof(&compressed_proof).expect("proof repack"); + let scalar_layout = generator.repacked_proof_scalar_layout_for_test(); + let params_verifier = srs.verifier_params(); + + PropertyPoseidonFixture { + compressed_proof, + proof, + scalar_layout, + instances: vec![instance], + params_verifier, + vk, + embedded_verifier_solidity, + embedded_trace_verifier_solidity, + embedded_gas_verifier_solidity, + separate_verifier_solidity, + gas_separate_verifier_solidity, + vk_solidity, + quotient_verifier_solidity, + quotient_evaluator_solidity, + trace_quotient_evaluator_solidity, + trace_quotient_verifier_solidity, + trace_quotient_vk_solidity, + trace_verifier_solidity, + trace_vk_solidity, + } +} + +#[test] +fn every_proof_scalar_rejects_fr_modulus() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture = create_property_poseidon_fixture(); + let r_be = fr_modulus_be_word(); + let scalar_offsets = proof_scalar_offsets(&fixture); + assert!( + !scalar_offsets.is_empty(), + "fixture proof should expose scalar fields to range-check" + ); + + let mut evm = deployed_separate_verifier(&fixture); + if !deployed_call_accepts(&mut evm, &fixture, &fixture.proof, "valid proof") { + return; + } + + for (name, offset) in scalar_offsets { + let mut bad_proof = fixture.proof.clone(); + bad_proof[offset..offset + 0x20].copy_from_slice(&r_be); + assert_deployed_call_rejects( + &mut evm, + &fixture, + &bad_proof, + &format!("{name} scalar equal to Fr modulus at proof offset {offset}"), + ); + } +} + +#[test] +fn every_proof_scalar_rejects_boundary_values() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture = create_property_poseidon_fixture(); + let scalar_offsets = proof_scalar_offsets(&fixture); + assert!( + !scalar_offsets.is_empty(), + "fixture proof should expose scalar fields to mutate" + ); + + let mut evm = deployed_separate_verifier(&fixture); + if !deployed_call_accepts(&mut evm, &fixture, &fixture.proof, "valid proof") { + return; + } + + let r = fr_modulus_u256(); + let static_variants = [ + ("zero", U256::from(0u64).to_be_bytes::<32>()), + ("one", U256::from(1u64).to_be_bytes::<32>()), + ("Fr_minus_one", (r - U256::from(1u64)).to_be_bytes::<32>()), + ]; + + for (name, offset) in scalar_offsets { + let original_word: [u8; 32] = + fixture.proof[offset..offset + 0x20].try_into().expect("proof scalar word"); + let original = U256::from_be_bytes(original_word); + let original_plus_r = original + r; + + for (variant, word) in static_variants + .into_iter() + .chain([("original_plus_Fr", original_plus_r.to_be_bytes::<32>())]) + { + if word == original_word { + continue; + } + let mut bad_proof = fixture.proof.clone(); + bad_proof[offset..offset + 0x20].copy_from_slice(&word); + assert_deployed_call_rejects( + &mut evm, + &fixture, + &bad_proof, + &format!("{name} scalar replaced with {variant} at proof offset {offset}"), + ); + } + } +} + +#[test] +fn representative_proof_section_mutations_are_rejected() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture = create_property_poseidon_fixture(); + let sections = representative_proof_section_offsets(&fixture); + assert!( + !sections.is_empty(), + "fixture should expose representative proof sections" + ); + + let mut evm = deployed_separate_verifier(&fixture); + if !deployed_call_accepts(&mut evm, &fixture, &fixture.proof, "valid proof") { + return; + } + + for (section, offset) in sections { + let mut bad_proof = fixture.proof.clone(); + bad_proof[offset] ^= 0x01; + assert_deployed_call_rejects( + &mut evm, + &fixture, + &bad_proof, + &format!("mutated proof section `{section}` at proof offset {offset}"), + ); + } +} + +#[test] +fn separate_verifier_adversarial_calldata_variants_are_rejected() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture = create_property_poseidon_fixture(); + let mut evm = deployed_separate_verifier(&fixture); + let valid = encode_calldata(&fixture.proof, &fixture.instances); + if !deployed_raw_call_accepts(&mut evm, valid.clone(), "valid separate-verifier calldata") { + return; + } + + let mut wrong_instances = fixture.instances.clone(); + wrong_instances[0] += F::ONE; + assert_solidity_rejects( + call_deployed_verifier(&mut evm, &fixture.proof, &wrong_instances), + "wrong instance", + ); + + let r_be = fr_modulus_be_word(); + let mut noncanonical_instance = valid.clone(); + let instance_word_start = first_instance_word_start(&fixture.proof); + noncanonical_instance[instance_word_start..instance_word_start + 0x20].copy_from_slice(&r_be); + assert_solidity_rejects( + call_deployed_verifier_raw(&mut evm, noncanonical_instance), + "instance scalar equal to Fr modulus", + ); + + let mut endian_swapped_instance = valid.clone(); + let mut swapped_word: [u8; 32] = valid[instance_word_start..instance_word_start + 0x20] + .try_into() + .expect("instance word"); + swapped_word.reverse(); + if swapped_word == valid[instance_word_start..instance_word_start + 0x20] { + swapped_word[31] ^= 0x01; + } + endian_swapped_instance[instance_word_start..instance_word_start + 0x20] + .copy_from_slice(&swapped_word); + assert_solidity_rejects( + call_deployed_verifier_raw(&mut evm, endian_swapped_instance), + "public input encoded with reversed endianness", + ); + + let mut trailing_bytes = valid.clone(); + trailing_bytes.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]); + + let mut proof_head_overlap = valid.clone(); + overwrite_u256_word(&mut proof_head_overlap, 0x04, 0x20); + + let mut proof_head_shifted_without_gap = valid.clone(); + overwrite_u256_word(&mut proof_head_shifted_without_gap, 0x04, 0x60); + + let mut instances_head_overlap = valid.clone(); + overwrite_u256_word(&mut instances_head_overlap, 0x24, 0x40); + + let mut instances_head_shifted = valid.clone(); + overwrite_u256_word( + &mut instances_head_shifted, + 0x24, + canonical_instances_head(&fixture.proof) as u64 + 0x20, + ); + + let shifted_valid_abi = calldata_with_shifted_dynamic_heads(&valid, &fixture.proof); + + for (name, calldata) in [ + ("trailing bytes", trailing_bytes), + ("proof head overlaps ABI head", proof_head_overlap), + ( + "proof head shifted without matching gap", + proof_head_shifted_without_gap, + ), + ("instances head overlaps proof", instances_head_overlap), + ("instances head shifted", instances_head_shifted), + ( + "valid ABI with noncanonical dynamic offsets", + shifted_valid_abi, + ), + ] { + assert_solidity_rejects(call_deployed_verifier_raw(&mut evm, calldata), name); + } +} + +#[test] +fn verifier_rejects_when_x_is_forced_to_domain_root() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture = create_property_poseidon_fixture(); + let forced_x_verifier = + verifier_source_with_x_forced_to_one(&fixture.separate_verifier_solidity); + let mut evm = deploy_separate_verifier_from_sources(&forced_x_verifier, &fixture.vk_solidity); + + assert_solidity_rejects( + call_deployed_verifier(&mut evm, &fixture.proof, &fixture.instances), + "verifier with x forced to domain root should hit zero Lagrange denominator", + ); +} + +#[test] +fn every_proof_g1_rejects_noncanonical_coordinates() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture = create_property_poseidon_fixture(); + let layout = proof_g1_layout(&fixture); + + assert_eq!( + layout.compressed_offsets.len(), + layout.repacked_offsets.len(), + "native/Solidity proof G1 offset schedules must agree" + ); + + let native_bad = [0xffu8; 48]; + let mut evm = deployed_separate_verifier(&fixture); + if !deployed_call_accepts(&mut evm, &fixture, &fixture.proof, "valid proof") { + return; + } + + for (idx, (&compressed_offset, &repacked_offset)) in + layout.compressed_offsets.iter().zip(layout.repacked_offsets.iter()).enumerate() + { + let mut bad_native = fixture.compressed_proof.clone(); + bad_native[compressed_offset..compressed_offset + 48].copy_from_slice(&native_bad); + assert_native_poseidon_rejects( + &fixture, + &bad_native, + &format!("native noncanonical G1 idx={idx} compressed_offset={compressed_offset}"), + ); + + let mut bad_solidity = fixture.proof.clone(); + // EIP-2537 padded G1 words require the top 16 bytes of x_hi/y_hi + // to be zero. Set one padding byte so the Solidity canonicality + // guard must reject before the point can enter the transcript. + bad_solidity[repacked_offset] = 1; + assert_deployed_call_rejects( + &mut evm, + &fixture, + &bad_solidity, + &format!("Solidity noncanonical G1 idx={idx} repacked_offset={repacked_offset}"), + ); + } +} + +#[test] +fn every_proof_g1_rejects_off_curve_coordinates() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture = create_property_poseidon_fixture(); + let layout = proof_g1_layout(&fixture); + let native_bad = compressed_off_curve_g1_bytes(); + let solidity_bad = eip2537_padded_off_curve_g1_bytes(); + + let mut evm = deployed_separate_verifier(&fixture); + if !deployed_call_accepts(&mut evm, &fixture, &fixture.proof, "valid proof") { + return; + } + + for (idx, (&compressed_offset, &repacked_offset)) in + layout.compressed_offsets.iter().zip(layout.repacked_offsets.iter()).enumerate() + { + let mut bad_native = fixture.compressed_proof.clone(); + bad_native[compressed_offset..compressed_offset + 48].copy_from_slice(&native_bad); + assert_native_poseidon_rejects( + &fixture, + &bad_native, + &format!("native off-curve G1 idx={idx} compressed_offset={compressed_offset}"), + ); + + let mut bad_solidity = fixture.proof.clone(); + bad_solidity[repacked_offset..repacked_offset + 128].copy_from_slice(&solidity_bad); + assert_deployed_call_rejects( + &mut evm, + &fixture, + &bad_solidity, + &format!("Solidity off-curve G1 idx={idx} repacked_offset={repacked_offset}"), + ); + } +} + +/// Produce a native Poseidon proof and verify it before Solidity tests use it. +fn generate_poseidon_proof( + srs: &PoseidonParams, + relation: &PoseidonExample, + vk: &MidnightVK, +) -> (Vec, F) { + let pk = midnight_zk_stdlib::setup_pk(relation, vk); + let mut rng = ChaCha8Rng::seed_from_u64(42); + let witness: [F; 3] = core::array::from_fn(|_| F::random(&mut rng)); + let instance = as HashCPU>::hash(&witness); + let prover_rng = ChaCha8Rng::seed_from_u64(0xdebd); + let proof = midnight_zk_stdlib::prove::( + srs, &pk, relation, &instance, witness, prover_rng, + ) + .expect("proof generation should not fail"); + + midnight_zk_stdlib::verify::( + &srs.verifier_params(), + vk, + &instance, + None, + &proof, + ) + .expect("generated proof should verify natively"); + + (proof, instance) +} + +/// Property case: a valid Poseidon proof is accepted. +fn run_property_poseidon_positive_case(separate: bool, seed: u64) { + let fixture = create_property_poseidon_fixture(); + let mut deployed = deployed_property_poseidon_verifier(&fixture, separate); + assert_deployed_call_accepts( + &mut deployed, + &fixture, + &fixture.proof, + &format!("valid proof seed={seed} separate={separate}"), + ); +} + +/// Property case: mutating the public instance is rejected. +fn run_property_poseidon_wrong_instance_case(separate: bool, seed: u64) { + let fixture = create_property_poseidon_fixture(); + let mut deployed = deployed_property_poseidon_verifier(&fixture, separate); + assert_deployed_call_accepts( + &mut deployed, + &fixture, + &fixture.proof, + &format!("valid proof before wrong-instance mutation seed={seed} separate={separate}"), + ); + + let mut bad_instances = fixture.instances.clone(); + bad_instances[0] += F::ONE; + + let output = call_deployed_verifier(&mut deployed, &fixture.proof, &bad_instances); + assert_solidity_rejects( + output, + &format!("wrong instance seed={seed} separate={separate}"), + ); +} + +/// Property case: flipping one proof bit is rejected. +fn run_property_poseidon_malleated_proof_case(separate: bool, seed: u64, bit_idx: usize) { + let fixture = create_property_poseidon_fixture(); + let mut deployed = deployed_property_poseidon_verifier(&fixture, separate); + assert_deployed_call_accepts( + &mut deployed, + &fixture, + &fixture.proof, + &format!("valid proof before proof mutation seed={seed} separate={separate}"), + ); + + let mut bad_proof = fixture.proof.clone(); + let byte_idx = bit_idx / 8 % bad_proof.len(); + let bit_mask = 1u8 << (bit_idx % 8); + bad_proof[byte_idx] ^= bit_mask; + + let output = call_deployed_verifier(&mut deployed, &bad_proof, &fixture.instances); + assert_solidity_rejects( + output, + &format!("malleated proof seed={seed} separate={separate}"), + ); +} + +/// Property case: mutating the linked VK source prevents verification. +fn run_property_poseidon_wrong_vk_case(seed: u64) { + let fixture = create_property_poseidon_fixture(); + let mut baseline = deployed_separate_verifier(&fixture); + assert_deployed_call_accepts( + &mut baseline, + &fixture, + &fixture.proof, + &format!("valid proof before wrong-vk mutation seed={seed}"), + ); + + let mutated_vk_solidity = mutate_first_large_hex_literal(&fixture.vk_solidity, seed as usize); + assert_separate_verifier_rejects_vk_mutation( + &fixture.separate_verifier_solidity, + &mutated_vk_solidity, + &fixture.proof, + &fixture.instances, + &format!("wrong vk seed={seed}"), + ); +} + +/// Property case: changing only the VK digest literal breaks the pinned +/// verifier. +fn run_separate_vk_digest_prefix_affects_verification_case(seed: u64) { + let fixture = create_property_poseidon_fixture(); + let original = call_separate_verifier( + &fixture.separate_verifier_solidity, + &fixture.vk_solidity, + &fixture.proof, + &fixture.instances, + ); + assert_solidity_accepts(original, &format!("valid separate vk seed={seed}")); + + assert_separate_verifier_rejects_vk_mutation( + &fixture.separate_verifier_solidity, + &mutate_vk_digest_literal_only(&fixture.vk_solidity), + &fixture.proof, + &fixture.instances, + &format!("digest-only mutated separate vk seed={seed}"), + ); +} + +/// Compile, deploy, and call an embedded-VK verifier. +fn call_embedded_verifier( + verifier_solidity: &str, + proof: &[u8], + instances: &[F], +) -> Result, ()> { + call_embedded_verifier_raw(verifier_solidity, encode_calldata(proof, instances)) +} + +/// Compile, deploy, and call an embedded-VK verifier with prebuilt calldata. +fn call_embedded_verifier_raw(verifier_solidity: &str, calldata: Vec) -> Result, ()> { + let mut deployed = deploy_embedded_verifier_from_source(verifier_solidity); + call_deployed_verifier_raw(&mut deployed, calldata) +} + +/// Compile, deploy, and call a separate-VK verifier pair. +fn call_separate_verifier( + verifier_solidity: &str, + vk_solidity: &str, + proof: &[u8], + instances: &[F], +) -> Result, ()> { + let mut deployed = deploy_separate_verifier_from_sources(verifier_solidity, vk_solidity); + call_deployed_verifier(&mut deployed, proof, instances) +} + +/// Assert a separate verifier constructor rejects a bad VK dependency. +fn assert_separate_verifier_rejects_vk_dependency( + verifier_solidity: &str, + vk_solidity: &str, + context: &str, +) { + let vk_creation_code = compile_solidity(vk_solidity); + let verifier_creation_code = compile_solidity(verifier_solidity); + let mut evm = Evm::default(); + let vk_address = evm.create(vk_creation_code); + let result = std::panic::catch_unwind(AssertUnwindSafe(|| { + evm.create_with_address_arg(verifier_creation_code, vk_address); + })); + assert!( + result.is_err(), + "separate verifier constructor accepted invalid VK dependency: {context}" + ); +} + +/// Assert a pinned quotient verifier constructor rejects a bad dependency set. +fn assert_pinned_quotient_constructor_rejects( + verifier_solidity: &str, + vk_solidity: &str, + quotient_solidity: &str, + context: &str, +) { + let verifier_creation_code = compile_solidity(verifier_solidity); + let vk_creation_code = compile_solidity(vk_solidity); + let quotient_creation_code = compile_solidity(quotient_solidity); + assert_pinned_quotient_constructor_rejects_address_args( + &verifier_creation_code, + &vk_creation_code, + "ient_creation_code, + context, + ); +} + +/// Assert constructor rejection from already-compiled dependency bytecode. +fn assert_pinned_quotient_constructor_rejects_address_args( + verifier_creation_code: &[u8], + vk_arg_creation_code: &[u8], + quotient_arg_creation_code: &[u8], + context: &str, +) { + let result = std::panic::catch_unwind(AssertUnwindSafe(|| { + let mut evm = Evm::default(); + let vk_address = evm.create(vk_arg_creation_code.to_vec()); + let quotient_address = evm.create(quotient_arg_creation_code.to_vec()); + evm.create_with_two_address_args( + verifier_creation_code.to_vec(), + vk_address, + quotient_address, + ); + })); + assert!( + result.is_err(), + "pinned verifier constructor accepted invalid dependency: {context}" + ); +} + +/// Deployed verifier state shared by call helpers. +struct DeployedVerifier { + evm: Evm, + verifier_address: revm::primitives::Address, +} + +/// Deploy the cached separate verifier/VK fixture. +fn deployed_separate_verifier(fixture: &PropertyPoseidonFixture) -> DeployedVerifier { + deploy_separate_verifier_from_sources(&fixture.separate_verifier_solidity, &fixture.vk_solidity) +} + +/// Deploy either cached embedded or separate Poseidon verifier. +fn deployed_property_poseidon_verifier( + fixture: &PropertyPoseidonFixture, + separate: bool, +) -> DeployedVerifier { + if separate { + deployed_separate_verifier(fixture) + } else { + deploy_embedded_verifier_from_source(&fixture.embedded_verifier_solidity) + } +} + +/// Compile and deploy one embedded verifier source. +fn deploy_embedded_verifier_from_source(verifier_solidity: &str) -> DeployedVerifier { + let mut evm = Evm::default(); + let verifier_address = evm.create(compile_solidity(verifier_solidity)); + DeployedVerifier { + evm, + verifier_address, + } +} + +/// Compile and deploy separate VK then verifier source. +fn deploy_separate_verifier_from_sources( + verifier_solidity: &str, + vk_solidity: &str, +) -> DeployedVerifier { + let mut evm = Evm::default(); + let vk_address = evm.create(compile_solidity(vk_solidity)); + let verifier_address = + evm.create_with_address_arg(compile_solidity(verifier_solidity), vk_address); + DeployedVerifier { + evm, + verifier_address, + } +} + +/// Encode calldata and call an already deployed verifier. +fn call_deployed_verifier( + deployed: &mut DeployedVerifier, + proof: &[u8], + instances: &[F], +) -> Result, ()> { + call_deployed_verifier_raw(deployed, encode_calldata(proof, instances)) +} + +/// Call an already deployed verifier with prebuilt calldata. +fn call_deployed_verifier_raw( + deployed: &mut DeployedVerifier, + calldata: Vec, +) -> Result, ()> { + match deployed + .evm + .try_call_with_gas(deployed.verifier_address, calldata, 5_000_000_000) + { + CallOutcome::Success { output, .. } => Ok(output), + CallOutcome::Revert { gas_used, output } => { + eprintln!( + "verifier reverted with gas_used = {gas_used}, output = 0x{}", + hex::encode(output) + ); + Err(()) + } + CallOutcome::Halt { gas_used, reason } => { + panic!("verifier halted with gas_used = {gas_used}, reason = {reason}"); + } + } +} + +/// Return whether a deployed verifier accepts a proof, skipping on bad +/// baseline. +fn deployed_call_accepts( + deployed: &mut DeployedVerifier, + fixture: &PropertyPoseidonFixture, + proof: &[u8], + context: &str, +) -> bool { + let output = call_deployed_verifier(deployed, proof, &fixture.instances); + solidity_output_is_true_or_skip(output, context) +} + +/// Assert a deployed verifier accepts a proof. +fn assert_deployed_call_accepts( + deployed: &mut DeployedVerifier, + fixture: &PropertyPoseidonFixture, + proof: &[u8], + context: &str, +) { + let output = call_deployed_verifier(deployed, proof, &fixture.instances); + assert_solidity_accepts(output, context); +} + +/// Return whether a raw deployed call accepts, skipping on bad baseline. +fn deployed_raw_call_accepts( + deployed: &mut DeployedVerifier, + calldata: Vec, + context: &str, +) -> bool { + let output = call_deployed_verifier_raw(deployed, calldata); + solidity_output_is_true_or_skip(output, context) +} + +/// Interpret verifier output as `true`, or request that adversarial test skip. +fn solidity_output_is_true_or_skip(output: Result, ()>, context: &str) -> bool { + let expected_true = [vec![0; 31], vec![1]].concat(); + match output { + Ok(bytes) if bytes == expected_true => true, + Ok(bytes) => { + eprintln!( + "skipping adversarial Solidity test: baseline `{context}` returned 0x{}", + hex::encode(bytes) + ); + false + } + Err(()) => { + eprintln!( + "skipping adversarial Solidity test: baseline `{context}` reverted or halted" + ); + false + } + } +} + +/// Assert a deployed verifier rejects a proof mutation. +fn assert_deployed_call_rejects( + deployed: &mut DeployedVerifier, + fixture: &PropertyPoseidonFixture, + proof: &[u8], + context: &str, +) { + let output = call_deployed_verifier(deployed, proof, &fixture.instances); + assert_solidity_rejects(output, context); +} + +/// Assert a separate verifier rejects proofs under a mutated VK contract. +fn assert_separate_verifier_rejects_vk_mutation( + verifier_solidity: &str, + vk_solidity: &str, + proof: &[u8], + instances: &[F], + context: &str, +) { + let vk_creation_code = compile_solidity(vk_solidity); + let verifier_creation_code = compile_solidity(verifier_solidity); + let mut evm = Evm::default(); + let vk_address = evm.create(vk_creation_code); + let verifier_address = match std::panic::catch_unwind(AssertUnwindSafe(|| { + evm.create_with_address_arg(verifier_creation_code, vk_address) + })) { + Ok(address) => address, + Err(_) => return, + }; + let mut deployed = DeployedVerifier { + evm, + verifier_address, + }; + assert_solidity_rejects( + call_deployed_verifier(&mut deployed, proof, instances), + context, + ); +} + +/// Assert the native verifier also rejects a compressed proof mutation. +fn assert_native_poseidon_rejects( + fixture: &PropertyPoseidonFixture, + compressed_proof: &[u8], + context: &str, +) { + let result = std::panic::catch_unwind(AssertUnwindSafe(|| { + midnight_zk_stdlib::verify::( + &fixture.params_verifier, + &fixture.vk, + &fixture.instances[0], + None, + compressed_proof, + ) + })); + + if let Ok(Ok(())) = result { + panic!("native verifier accepted malformed proof: {context}"); + } +} + +#[derive(Clone, Debug)] +struct ProofG1Layout { + compressed_offsets: Vec, + repacked_offsets: Vec, +} + +/// Return named scalar-word offsets in the Solidity-facing proof. +fn proof_scalar_offsets(fixture: &PropertyPoseidonFixture) -> Vec<(String, usize)> { + let layout = fixture.scalar_layout; + let mut offsets = Vec::with_capacity(layout.num_evals + layout.num_point_sets); + offsets.extend( + (0..layout.num_evals).map(|idx| (format!("eval[{idx}]"), layout.eval_offset + idx * 0x20)), + ); + offsets.extend( + (0..layout.num_point_sets) + .map(|idx| (format!("q_eval[{idx}]"), layout.q_eval_offset + idx * 0x20)), + ); + assert!( + offsets.iter().all(|(_, offset)| offset + 0x20 <= fixture.proof.len()), + "scalar offsets must be inside the Solidity proof" + ); + offsets +} + +/// Return compressed and repacked G1 offsets for proof mutation tests. +fn proof_g1_layout(fixture: &PropertyPoseidonFixture) -> ProofG1Layout { + let prefix_g1_count = fixture.scalar_layout.eval_offset / 0x80; + assert_eq!( + fixture.scalar_layout.eval_offset % 0x80, + 0, + "G1 prefix must end on a repacked G1 boundary" + ); + + let f_com_repacked_offset = + fixture.scalar_layout.eval_offset + fixture.scalar_layout.num_evals * 0x20; + let pi_repacked_offset = + fixture.scalar_layout.q_eval_offset + fixture.scalar_layout.num_point_sets * 0x20; + assert_eq!( + fixture.scalar_layout.q_eval_offset, + f_com_repacked_offset + 0x80, + "q_eval block must follow f_com" + ); + + let mut repacked_offsets = (0..prefix_g1_count).map(|idx| idx * 0x80).collect::>(); + repacked_offsets.push(f_com_repacked_offset); + repacked_offsets.push(pi_repacked_offset); + + let f_com_compressed_offset = prefix_g1_count * 48 + fixture.scalar_layout.num_evals * 0x20; + let pi_compressed_offset = + f_com_compressed_offset + 48 + fixture.scalar_layout.num_point_sets * 0x20; + let mut compressed_offsets = (0..prefix_g1_count).map(|idx| idx * 48).collect::>(); + compressed_offsets.push(f_com_compressed_offset); + compressed_offsets.push(pi_compressed_offset); + + assert!( + compressed_offsets + .iter() + .all(|offset| offset + 48 <= fixture.compressed_proof.len()), + "compressed G1 offsets must be inside the native proof" + ); + assert!( + repacked_offsets.iter().all(|offset| offset + 0x80 <= fixture.proof.len()), + "repacked G1 offsets must be inside the Solidity proof" + ); + + ProofG1Layout { + compressed_offsets, + repacked_offsets, + } +} + +/// Find a canonical compressed x-coordinate that does not decode to a G1 point. +fn compressed_off_curve_g1_bytes() -> [u8; 48] { + use group::GroupEncoding; + use midnight_curves::G1Affine; + + for x in 1u64..10_000 { + let mut candidate = [0u8; 48]; + candidate[40..48].copy_from_slice(&x.to_be_bytes()); + // BLS compressed flag, no infinity flag. The remaining high bits of + // the x-coordinate are zero, so the coordinate is canonical. + candidate[0] |= 0x80; + + let mut repr = ::Repr::default(); + repr.as_mut().copy_from_slice(&candidate); + if bool::from(G1Affine::from_bytes(&repr).is_none()) { + return candidate; + } + } + + panic!("failed to find a canonical compressed x-coordinate with no G1 point"); +} + +/// Return a padded uncompressed point that is canonical but off curve. +fn eip2537_padded_off_curve_g1_bytes() -> [u8; 128] { + let mut out = [0u8; 128]; + // EIP-2537 padded uncompressed layout is x_hi, x_lo, y_hi, y_lo. + // (0, 1) is field-canonical but not on BLS12-381 G1, whose affine + // equation has b = 4, so the G1 precompiles must reject it. + out[127] = 1; + out +} + +/// Assert a Solidity verifier call returned ABI-encoded true. +fn assert_solidity_accepts(output: Result, ()>, context: &str) { + let expected_true = [vec![0; 31], vec![1]].concat(); + match output { + Ok(bytes) => assert_eq!(bytes, expected_true, "{context}"), + Err(()) => panic!("solidity call panicked unexpectedly: {context}"), + } +} + +/// Assert a Solidity verifier call reverted or halted. +fn assert_solidity_rejects(output: Result, ()>, context: &str) { + assert!( + output.is_err(), + "invalid proof/calldata returned instead of reverting: {context}" + ); +} + +/// Flip one nibble in a large hex literal for broad source-mutation tests. +fn mutate_first_large_hex_literal(solidity: &str, ordinal_seed: usize) -> String { + let bytes = solidity.as_bytes(); + let mut matches = Vec::new(); + for start in 0..bytes.len().saturating_sub(2) { + if bytes[start] == b'0' && bytes[start + 1] == b'x' { + let mut end = start + 2; + while end < bytes.len() && bytes[end].is_ascii_hexdigit() { + end += 1; + } + if end - (start + 2) >= 64 { + matches.push((start, end)); + } + } + } + + assert!( + !matches.is_empty(), + "no 64-byte hex literal found to mutate" + ); + let (start, end) = matches[ordinal_seed % matches.len()]; + let mut mutated = solidity.to_owned().into_bytes(); + let idx = end - 1 - ordinal_seed % ((end - start - 2).min(16)); + mutated[idx] = if mutated[idx] == b'0' { b'1' } else { b'0' }; + String::from_utf8(mutated).unwrap() +} + +/// Mutate only the VK digest literal in rendered VK source. +fn mutate_vk_digest_literal_only(solidity: &str) -> String { + mutate_value_hex_literal_on_line_containing(solidity, "vk_digest") +} + +/// Mutate the value literal on the first rendered `mstore` line with `marker`. +fn mutate_value_hex_literal_on_line_containing(solidity: &str, marker: &str) -> String { + let (line_start, line_end, _) = solidity + .lines() + .scan(0usize, |offset, line| { + let start = *offset; + *offset += line.len() + 1; + Some((start, start + line.len(), line)) + }) + .find(|(_, _, line)| line.contains("mstore(") && line.contains(marker)) + .unwrap_or_else(|| panic!("mstore line marker not found: {marker}")); + let line = &solidity[line_start..line_end]; + let line_before_comment = line.split_once("//").map(|(code, _)| code).unwrap_or(line); + let bytes = line_before_comment.as_bytes(); + let mut value_literal = None; + for start in 0..bytes.len().saturating_sub(1) { + if bytes[start] != b'0' || bytes[start + 1] != b'x' { + continue; + } + let mut end = start + 2; + while end < bytes.len() && bytes[end].is_ascii_hexdigit() { + end += 1; + } + if end - (start + 2) >= 64 { + value_literal = Some((start + 2, end - (start + 2))); + } + } + let (rel_hex_start, hex_len) = + value_literal.unwrap_or_else(|| panic!("line `{marker}` missing value hex literal")); + let abs_hex_start = line_start + rel_hex_start; + + let mut mutated = solidity.as_bytes().to_vec(); + let last = abs_hex_start + hex_len - 1; + mutated[last] = if mutated[last] == b'0' { b'1' } else { b'0' }; + String::from_utf8(mutated).unwrap() +} + +#[test] +fn vk_payload_mutator_targets_mstore_value_not_payload_offset() { + let value = format!("0x{:064x}", 0u8); + let old_shape = format!("mstore(0x0000, {value}) // vk_digest"); + let new_shape = format!("mstore(add(payload, 0x0000), {value}) // vk_digest"); + + for source in [old_shape, new_shape] { + let mutated = mutate_value_hex_literal_on_line_containing(&source, "vk_digest"); + assert!( + mutated.contains("0x0000"), + "payload offset should be left intact: {mutated}" + ); + assert!( + mutated.ends_with("0001) // vk_digest"), + "mstore value should be mutated: {mutated}" + ); + } +} + +/// Replace one constructor smoke-test staticcall in rendered Solidity. +fn replace_required_precompile_staticcall( + solidity: &str, + needle: &str, + replacement: &str, +) -> String { + assert!( + solidity.contains(needle), + "required precompile smoke-test staticcall not found: {needle}" + ); + solidity.replacen(needle, replacement, 1) +} + +/// Create a proptest runner with fixture-friendly persistence settings. +fn new_property_test_runner() -> TestRunner { + let cases = env::var("POSEIDON_PBT_CASES") + .ok() + .and_then(|value| value.parse().ok()) + .unwrap_or(3); + TestRunner::new(ProptestConfig { + cases, + failure_persistence: None, + ..ProptestConfig::default() + }) +} + +/// Overwrite one ABI word with a small big-endian integer. +fn overwrite_u256_word(bytes: &mut [u8], start: usize, value: u64) { + bytes[start..start + 32].fill(0); + bytes[start + 24..start + 32].copy_from_slice(&value.to_be_bytes()); +} + +/// Return the BLS12-381 scalar-field modulus as one big-endian word. +fn fr_modulus_be_word() -> [u8; 32] { + hex::decode("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001") + .expect("fr modulus hex") + .try_into() + .expect("Fr modulus is one word") +} + +/// Return the BLS12-381 scalar-field modulus as U256. +fn fr_modulus_u256() -> U256 { + U256::from_be_bytes(fr_modulus_be_word()) +} + +/// Append a named proof section offset if that offset is not already covered. +fn push_unique_section( + sections: &mut Vec<(String, usize)>, + name: impl Into, + offset: usize, +) { + if sections.iter().all(|(_, existing)| *existing != offset) { + sections.push((name.into(), offset)); + } +} + +/// Return representative offsets spanning all major proof sections. +fn representative_proof_section_offsets(fixture: &PropertyPoseidonFixture) -> Vec<(String, usize)> { + let layout = fixture.scalar_layout; + assert!( + layout.eval_offset >= 0x80, + "fixture proof should contain at least one leading G1 commitment" + ); + + let mut sections = Vec::new(); + push_unique_section(&mut sections, "first transcript commitment", 0); + push_unique_section( + &mut sections, + "last quotient limb commitment", + layout.eval_offset - 0x80, + ); + if layout.num_evals != 0 { + push_unique_section( + &mut sections, + "first main evaluation scalar", + layout.eval_offset, + ); + push_unique_section( + &mut sections, + "last main evaluation scalar", + layout.eval_offset + (layout.num_evals - 1) * 0x20, + ); + } + + let f_com_offset = layout.eval_offset + layout.num_evals * 0x20; + push_unique_section(&mut sections, "KZG f_com commitment", f_com_offset); + + if layout.num_point_sets != 0 { + push_unique_section( + &mut sections, + "first KZG point-set evaluation scalar", + layout.q_eval_offset, + ); + push_unique_section( + &mut sections, + "last KZG point-set evaluation scalar", + layout.q_eval_offset + (layout.num_point_sets - 1) * 0x20, + ); + } + + push_unique_section( + &mut sections, + "KZG proof commitment pi", + layout.q_eval_offset + layout.num_point_sets * 0x20, + ); + + assert!( + sections.iter().all(|(_, offset)| *offset < fixture.proof.len()), + "representative proof section offsets must be inside the proof" + ); + sections +} + +/// Patch rendered Solidity so the x challenge is forced to one. +fn verifier_source_with_x_forced_to_one(solidity: &str) -> String { + let needle = "buf_len := squeeze_to(buf_len, X_MPTR)"; + assert_eq!( + solidity.matches(needle).count(), + 1, + "expected exactly one x challenge squeeze to patch" + ); + solidity.replacen( + needle, + "buf_len := squeeze_to(buf_len, X_MPTR)\n mstore(X_MPTR, 1)", + 1, + ) +} + +/// Canonical ABI head offset for the `instances` dynamic argument. +fn canonical_instances_head(proof: &[u8]) -> usize { + 0x40 + 0x20 + proof.len() +} + +/// Calldata byte offset of the first instance word. +fn first_instance_word_start(proof: &[u8]) -> usize { + 4 + canonical_instances_head(proof) + 0x20 +} + +/// Build malformed calldata with valid payloads but shifted dynamic heads. +fn calldata_with_shifted_dynamic_heads(valid: &[u8], proof: &[u8]) -> Vec { + let mut shifted = valid.to_vec(); + shifted.splice(4 + 0x40..4 + 0x40, [0u8; 0x20]); + overwrite_u256_word(&mut shifted, 0x04, 0x60); + overwrite_u256_word( + &mut shifted, + 0x24, + canonical_instances_head(proof) as u64 + 0x20, + ); + shifted +} + +/// Return whether Poseidon EVM tests have all host prerequisites. +fn poseidon_inputs_available_for_evm() -> bool { + if !env_flag_enabled(RUN_EVM_TESTS_ENV) { + eprintln!("skipping Poseidon Solidity property test: set {RUN_EVM_TESTS_ENV}=1 to run it"); + return false; + } + if !poseidon_srs_available() { + return false; + } + if !solc_available() { + eprintln!("skipping Poseidon Solidity property test: solc not found"); + return false; + } + true +} + +/// Parse common truthy environment flag values. +fn env_flag_enabled(name: &str) -> bool { + env::var(name) + .map(|value| { + matches!( + value.to_ascii_lowercase().as_str(), + "1" | "true" | "yes" | "on" + ) + }) + .unwrap_or(false) +} + +/// Return whether the Poseidon test SRS can be found on disk. +fn poseidon_srs_available() -> bool { + let srs_dir = PathBuf::from(srs_dir()); + let exact_srs_path = srs_dir.join(format!("bls_filecoin_2p{POSEIDON_K}")); + let fallback_srs_path = srs_dir.join("bls_filecoin_2p19"); + if !exact_srs_path.exists() && !fallback_srs_path.exists() { + eprintln!( + "skipping Poseidon Solidity property test: SRS not found at {} or {}", + exact_srs_path.display(), + fallback_srs_path.display() + ); + return false; + } + true +} + +/// Return whether the configured pinned solc is available. +fn solc_available() -> bool { + pinned_solc_available() +} + +/// Resolve the SRS directory used by fixture setup. +fn srs_dir() -> String { + if let Ok(dir) = env::var("SRS_DIR") { + return dir; + } + + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../zk_stdlib/examples/assets") + .to_string_lossy() + .into_owned() +} diff --git a/proofs/solidity-verifier/src/trace_replay.rs b/proofs/solidity-verifier/src/trace_replay.rs deleted file mode 100644 index 4d270d740..000000000 --- a/proofs/solidity-verifier/src/trace_replay.rs +++ /dev/null @@ -1,296 +0,0 @@ -//! Generic Fiat-Shamir replay helper used by the cross-trace tests -//! (poseidon, IVC). -//! -//! Given a midnight-proofs `VerifyingKey` and a Keccak256-transcript -//! proof produced for it, this walks the transcript exactly the way -//! `midnight-proofs::plonk::verify` would — common-input the VK -//! digest, common-input the committed instance commitment(s) and the -//! non-committed instance, then loop through phases / lookups / -//! permutation / trashcans / quotient limbs, then read all -//! evaluations, and finally drive the KZG multi-open transcript -//! sequence (`x1`, `x2`, `f_com`, `x3`, `q_evals`, `x4`, `pi`) — and -//! records every challenge / scalar read / point read into a -//! [`Trace`]. -//! -//! The function is **CS-driven**, not circuit-specific: it walks the -//! `ConstraintSystem` exposed by the VK so the same code handles -//! `numLookups == 1` (poseidon, RSA) and `numLookups == 2` (IVC) and -//! any future circuit shape that fits the §7.2 invariants in -//! `ARCHITECTURE.md`. -//! -//! Used by: -//! -//! * `tests/forge.rs::rust_and_solidity_traces_match` — poseidon end-to-end -//! transcript-level equivalence. -//! * `tests/ivc.rs::rust_and_solidity_ivc_traces_match` — IVC -//! final-step Keccak256 transcript-level equivalence. - -use ff::PrimeField; -use group::Group; -use midnight_curves::{Bls12, Fq, G1Projective}; -#[cfg(feature = "fewer-point-sets")] -use midnight_proofs::poly::Rotation; -use midnight_proofs::{plonk::VerifyingKey, poly::kzg::KZGCommitmentScheme}; - -use crate::{eip2537, trace::Trace, transcript::TracingTranscript}; - -/// Replay the verifier's Keccak256 Fiat-Shamir sequence and record -/// every transcript-observable event in a [`Trace`]. -/// -/// `nb_committed_instances` is the number of committed-instance -/// commitments the prover absorbed before the non-committed instance -/// columns. -pub fn replay_trace_from_vk( - vk: &VerifyingKey>, - proof: &[u8], - public_inputs: &[Fq], - nb_committed_instances: usize, -) -> Trace { - let cs = vk.cs(); - let mut t = TracingTranscript::init_from_bytes(proof); - - // --- hash VK digest into transcript ---------------------------------- - t.common_fq(&vk.transcript_repr()).unwrap(); - t.trace - .borrow_mut() - .intermediate("vk_repr", eip2537::fq_to_be_hex(&vk.transcript_repr())); - - // --- hash committed instance commitments (G1::identity() in tree) --- - let committed_identity: G1Projective = ::identity(); - for _ in 0..nb_committed_instances { - t.common_g1(&committed_identity).unwrap(); - } - - // --- hash non-committed instances ----------------------------------- - let normal_instance_columns = cs.num_instance_columns() - nb_committed_instances; - for column in 0..normal_instance_columns { - let values = if normal_instance_columns == 1 || column + 1 == normal_instance_columns { - public_inputs - } else { - &[] - }; - let inst_len = Fq::from_u128(values.len() as u128); - t.common_fq(&inst_len).unwrap(); - for v in values { - t.common_fq(v).unwrap(); - } - } - - // --- phases: read advice + squeeze advice challenges ---------------- - for (phase_idx, _phase) in cs.phases().enumerate() { - for i in 0..cs.num_advice_columns() { - let _ = t - .read_g1(&format!("advice[{i}]")) - .unwrap_or_else(|e| panic!("read advice[{i}] phase {phase_idx}: {e}")); - } - for _ in 0..cs.num_challenges() { - let _ = t.squeeze_fq("advice_challenge"); - } - } - let _theta = t.squeeze_fq("theta"); - - // --- lookup multiplicities ------------------------------------------ - for _ in 0..cs.lookups().len() { - let _ = t.read_g1("lookup_mult").unwrap(); - } - - let _beta = t.squeeze_fq("beta"); - let _gamma = t.squeeze_fq("gamma"); - - // --- permutation products (one G1 per chunk) ------------------------- - let perm_chunks = { - let chunk_len = cs.degree() - 2; - let cols = cs.permutation().get_columns().len(); - if cols == 0 { - 0 - } else { - cols.div_ceil(chunk_len) - } - }; - for _ in 0..perm_chunks { - let _ = t.read_g1("perm_product").unwrap(); - } - - // --- lookup commitments: per lookup, helpers (nChunks) + accumulator - for l in cs.lookups().iter() { - let nc = l.chunk_by_degree(cs.degree()).num_chunks(); - for _ in 0..nc { - let _ = t.read_g1("lookup_helper").unwrap(); - } - let _ = t.read_g1("lookup_acc").unwrap(); - } - - let _trash_ch = t.squeeze_fq("trash_challenge"); - for _ in 0..cs.trashcans().len() { - let _ = t.read_g1("trashcan").unwrap(); - } - let _y = t.squeeze_fq("y"); - - // --- quotient limbs --------------------------------------------------- - let nb_q = vk.get_domain().get_quotient_poly_degree(); - for _ in 0..nb_q { - let _ = t.read_g1("quotient_limb").unwrap(); - } - - let _x = t.squeeze_fq("x"); - - // --- read all evaluation scalars in Rust iterator order -------------- - let num_fixed_q = cs - .fixed_queries() - .iter() - .filter(|(c, _)| !cs.has_simple_selector_col(c.index())) - .count(); - // For committed-instance columns the verifier *reads* the eval - // from the transcript; for non-committed columns it computes the - // eval locally via Lagrange interpolation, so no transcript read - // is performed there. `nb_committed_instances` is the column - // index cutoff: instance queries on columns `< nb` are - // transcript reads. - let num_committed_instance_reads = cs - .instance_queries() - .iter() - .filter(|(col, _)| col.index() < nb_committed_instances) - .count(); - for _ in 0..num_committed_instance_reads { - let _ = t.read_fq("committed_instance_eval").unwrap(); - } - - for i in 0..cs.advice_queries().len() { - let _ = t.read_fq(&format!("advice_eval[{i}]")).unwrap(); - } - for i in 0..num_fixed_q { - let _ = t.read_fq(&format!("fixed_eval[{i}]")).unwrap(); - } - for i in 0..cs.permutation().get_columns().len() { - let _ = t.read_fq(&format!("perm_common[{i}]")).unwrap(); - } - for i in 0..perm_chunks { - let _ = t.read_fq("perm_cur").unwrap(); - let _ = t.read_fq("perm_next").unwrap(); - if i + 1 != perm_chunks { - let _ = t.read_fq("perm_last").unwrap(); - } - } - for l in cs.lookups().iter() { - let nc = l.chunk_by_degree(cs.degree()).num_chunks(); - let _ = t.read_fq("lookup_m_eval").unwrap(); - for _ in 0..nc { - let _ = t.read_fq("lookup_helper_eval").unwrap(); - } - let _ = t.read_fq("lookup_acc_eval").unwrap(); - let _ = t.read_fq("lookup_acc_next_eval").unwrap(); - } - for _ in 0..cs.trashcans().len() { - let _ = t.read_fq("trash_eval").unwrap(); - } - - // --- KZG multi-open -------------------------------------------------- - #[cfg(feature = "fewer-point-sets")] - { - let dummy_count = dummy_query_count(vk, _x, nb_committed_instances); - for _ in 0..dummy_count { - let _ = t.read_fq("dummy_eval").unwrap(); - } - } - let _x1 = t.squeeze_fq("x1"); - let _x2 = t.squeeze_fq("x2"); - let _f_com = t.read_g1("f_com").unwrap(); - let _x3 = t.squeeze_fq("x3"); - // Read remaining q_evals from the proof until only 48 bytes — - // exactly one G1 point — remain. - loop { - let buf = t.inner_mut().buffer(); - let remaining = buf.get_ref().len() as i64 - buf.position() as i64; - if remaining <= 48 { - break; - } - let _ = t.read_fq("q_eval").unwrap(); - } - let _x4 = t.squeeze_fq("x4"); - let _pi = t.read_g1("pi").unwrap(); - - t.trace() -} - -#[cfg(feature = "fewer-point-sets")] -fn dummy_query_count( - vk: &VerifyingKey>, - x: Fq, - nb_committed_instances: usize, -) -> usize { - let cs = vk.cs(); - let domain = vk.get_domain(); - let chunk_len = cs.degree() - 2; - let perm_chunks = { - let cols = cs.permutation().get_columns().len(); - if cols == 0 { - 0 - } else { - cols.div_ceil(chunk_len) - } - }; - let total_lookup_helpers: usize = - cs.lookups().iter().map(|l| l.chunk_by_degree(cs.degree()).num_chunks()).sum(); - - let advice_base = 0usize; - let instance_base = advice_base + cs.num_advice_columns(); - let perm_prod_base = instance_base + cs.num_instance_columns(); - let lookup_m_base = perm_prod_base + perm_chunks; - let lookup_helper_base = lookup_m_base + cs.lookups().len(); - let lookup_acc_base = lookup_helper_base + total_lookup_helpers; - let trash_base = lookup_acc_base + cs.lookups().len(); - let fixed_base = trash_base + cs.trashcans().len(); - let perm_common_base = fixed_base + cs.num_fixed_columns(); - let lin_base = perm_common_base + cs.permutation().get_columns().len(); - - let mut pairs = Vec::<(usize, Fq)>::new(); - for &(column, at) in cs.advice_queries() { - pairs.push((advice_base + column.index(), domain.rotate_omega(x, at))); - } - for &(column, at) in cs.instance_queries() { - if column.index() < nb_committed_instances { - pairs.push((instance_base + column.index(), domain.rotate_omega(x, at))); - } - } - - let x_next = domain.rotate_omega(x, Rotation::next()); - let x_last = domain.rotate_omega(x, Rotation(-((cs.blinding_factors() + 1) as i32))); - for i in 0..perm_chunks { - pairs.push((perm_prod_base + i, x)); - pairs.push((perm_prod_base + i, x_next)); - } - if perm_chunks > 1 { - for i in (0..perm_chunks - 1).rev() { - pairs.push((perm_prod_base + i, x_last)); - } - } - - let mut helper_offset = 0usize; - for (l_idx, lookup) in cs.lookups().iter().enumerate() { - pairs.push((lookup_m_base + l_idx, x)); - let chunks = lookup.chunk_by_degree(cs.degree()).num_chunks(); - for j in 0..chunks { - pairs.push((lookup_helper_base + helper_offset + j, x)); - } - helper_offset += chunks; - pairs.push((lookup_acc_base + l_idx, x)); - pairs.push((lookup_acc_base + l_idx, x_next)); - } - - for i in 0..cs.trashcans().len() { - pairs.push((trash_base + i, x)); - } - for &(column, at) in cs - .fixed_queries() - .iter() - .filter(|(col, _)| !cs.has_simple_selector_col(col.index())) - { - pairs.push((fixed_base + column.index(), domain.rotate_omega(x, at))); - } - for i in 0..cs.permutation().get_columns().len() { - pairs.push((perm_common_base + i, x)); - } - pairs.push((lin_base, x)); - - midnight_proofs::poly::kzg::compute_dummy_queries(&pairs).len() -} diff --git a/proofs/solidity-verifier/templates/contracts/Halo2QuotientEvaluator.sol b/proofs/solidity-verifier/templates/contracts/Halo2QuotientEvaluator.sol new file mode 100644 index 000000000..a7e0f52df --- /dev/null +++ b/proofs/solidity-verifier/templates/contracts/Halo2QuotientEvaluator.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.24; + +/// @title Split Halo2 quotient numerator evaluator. +/// @notice Reconstructs the scalar side of the linearization query for a generated verifier. +/// @dev This is the split-out implementation of the expensive +/// `partially_evaluate_identities` / `compute_linearization_commitment` side +/// from the Midfall Rust verifier: +/// - `midfall/proofs/src/plonk/mod.rs::partially_evaluate_identities` +/// - `midfall/proofs/src/plonk/linearization/verifier.rs::compute_linearization_commitment` +/// - `midfall/proofs/src/plonk/{permutation,logup,trash}.rs` +/// @dev The main verifier has already parsed calldata, checked proof scalar +/// ranges, sampled Fiat-Shamir challenges, loaded the VK payload, and computed +/// local Lagrange/public-input values before making the staticcall. +/// +/// Instead of receiving structured Solidity arguments, the evaluator receives +/// the verifier's memory frame as raw calldata: +/// +/// calldata[0..QUOTIENT_FRAME_LEN) +/// == memory[QUOTIENT_FRAME_BASE..QUOTIENT_FRAME_BASE+QUOTIENT_FRAME_LEN) +/// +/// The fallback copies that frame back into the same generated memory +/// addresses. All constants below are therefore memory addresses inside that +/// copied frame, not ABI offsets. +/// +/// Output is a compact fixed frame consumed by Halo2Verifier: +/// +/// word 0: QUOTIENT_MAGIC, a generated version/magic guard +/// word 1: linearization_expected_eval +/// word 2..: simple-selector accumulator scalars +/// +/// This contract reconstructs the Rust verifier's y-batched identity numerator +/// nu_y(x) and returns the linearization expected scalar -nu_y(x). It does not +/// evaluate or trust a quotient scalar h(x). +/// +/// The quotient limb commitments are handled by Halo2Verifier on the commitment +/// side as (1 - x^n) * sum_i x_split^i * Q_i. That is why this scalar side is +/// -nu_y(x), not h(x) = nu_y(x) / (x^n - 1). +/// +/// See docs/QUOTIENT_NUMERATOR_EVALUATOR.md for the full Rust/Solidity mapping. +contract Halo2QuotientEvaluator { + // BLS12-381 scalar field modulus. All arithmetic in this contract is over + // Fr and uses addmod/mulmod with this modulus. + uint256 internal constant FR_MODULUS = + 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001; + + // Start of the copied verifier-key payload in memory. The VK payload also + // carries the compact quotient VM constant/program tables used by the + // included numerator block. + uint256 internal constant VK_MPTR = {{ memory.vk_mptr }}; + + // Fiat-Shamir challenge slots. Halo2Verifier sampled these in transcript + // order before the external call. The evaluator only reads them. + uint256 internal constant CHALLENGE_MPTR = {{ memory.challenge_mptr }}; + uint256 internal constant THETA_MPTR = {{ memory.theta_mptr }}; + uint256 internal constant BETA_MPTR = {{ memory.beta_mptr }}; + uint256 internal constant GAMMA_MPTR = {{ memory.gamma_mptr }}; + uint256 internal constant TRASH_CHALLENGE_MPTR = {{ memory.trash_challenge_mptr }}; + uint256 internal constant Y_MPTR = {{ memory.y_mptr }}; + uint256 internal constant X_MPTR = {{ memory.x_mptr }}; + uint256 internal constant X1_MPTR = {{ memory.x1_mptr }}; + uint256 internal constant X2_MPTR = {{ memory.x2_mptr }}; + uint256 internal constant X3_MPTR = {{ memory.x3_mptr }}; + uint256 internal constant X4_MPTR = {{ memory.x4_mptr }}; + + // Common polynomial values at x. Halo2Verifier computes these once after + // sampling x and places them in the frame so the numerator block can share + // the exact Rust verifier inputs. + uint256 internal constant X_N_MPTR = {{ memory.x_n_mptr }}; + uint256 internal constant X_N_MINUS_1_INV_MPTR = {{ memory.x_n_minus_1_inv_mptr }}; + uint256 internal constant L_LAST_MPTR = {{ memory.l_last_mptr }}; + uint256 internal constant L_BLIND_MPTR = {{ memory.l_blind_mptr }}; + uint256 internal constant L_0_MPTR = {{ memory.l_0_mptr }}; + uint256 internal constant INSTANCE_EVAL_MPTR = {{ memory.instance_eval_mptr }}; + uint256 internal constant QUOTIENT_EVAL_MPTR = {{ memory.quotient_eval_mptr }}; + + // Proof evaluation table. Values are already decoded as canonical Fr words + // by Halo2Verifier. The generated numerator code indexes this table by the + // same query order as the Rust verifier. + uint256 internal constant REVERSED_EVALS_MPTR = {{ memory.reversed_evals_mptr }}; + + // Scratch/output region for simple-selector linearization accumulators. + // The numerator block writes one bucket per simple selector, then the + // fallback copies those buckets into the compact return frame. + uint256 internal constant SELECTOR_ACC_MPTR = {{ memory.selector_acc_mptr|hex() }}; + // Callee-local scratch for trace hooks. Trace-enabled verifier builds call + // this evaluator with CALL so quotient identity logs can be compared with + // the native Rust trace. Production verifier builds keep using STATICCALL + // and render this evaluator without trace hooks. + uint256 internal constant TRACE_U256_MPTR = {{ memory.quotient_return_mptr|hex() }}; + uint256 internal constant QUOTIENT_OUTPUT_MPTR = {{ memory.quotient_return_mptr|hex() }}; + + // External-call frame metadata. The main verifier calls this contract with + // exactly QUOTIENT_FRAME_LEN bytes starting at + // QUOTIENT_FRAME_BASE, then checks the return length and QUOTIENT_MAGIC. + uint256 internal constant QUOTIENT_FRAME_BASE = {{ quotient_external.frame_base|hex() }}; + uint256 internal constant QUOTIENT_FRAME_LEN = {{ quotient_external.frame_len|hex() }}; + uint256 internal constant QUOTIENT_OUTPUT_LEN = {{ quotient_external.output_len|hex() }}; + uint256 internal constant QUOTIENT_MAGIC = {{ quotient_external.magic|hex_padded(64) }}; + + /// @notice Evaluate the generated quotient numerator block for one verifier memory frame. + /// @dev Calldata is exactly the raw frame, not ABI-encoded arguments. Returns `QUOTIENT_MAGIC`, the linearization expected eval, and selector buckets. + /// @dev This fallback also uses generated absolute memory addresses and + /// returns directly from assembly. Its compact return frame starts at + /// `0x80`, preserving Solidity's reserved memory words. + fallback() external { + assembly ("memory-safe") { + // Reject malformed calls. This contract is not a general-purpose + // ABI endpoint; accepting partial or shifted frames would make the + // generated memory addresses point at the wrong data. + if iszero(eq(calldatasize(), QUOTIENT_FRAME_LEN)) { revert(0, 0) } + + // Rehydrate the verifier memory image. From this point onward the + // generated Yul can use the same MPTR constants as the monolithic + // verifier path. + calldatacopy(QUOTIENT_FRAME_BASE, 0, QUOTIENT_FRAME_LEN) + + let r := FR_MODULUS + + {%- if self.trace %} + // Trace-only side channel used by fixture comparison. Production + // split evaluators are called via STATICCALL and render without + // this LOG helper. + function trace_u256(id, value) { + mstore(TRACE_U256_MPTR, value) + log1(TRACE_U256_MPTR, 0x20, id) + } + {%- endif %} + + // This included block is the main body of the evaluator. It: + // 1. evaluates gate/permutation/lookup/trash identities in the + // same order as Rust `partially_evaluate_identities`; + // 2. y-batches fully evaluated identities into + // quotient_eval_numer; + // 3. y-batches simple-selector identities into + // SELECTOR_ACC_MPTR buckets; + // 4. writes -quotient_eval_numer to QUOTIENT_EVAL_MPTR. + // + // Depending on codegen settings, some identities are native Yul + // callbacks and the rest are executed by the compact q_program VM + // stored in the copied VK payload. + // + // The upstream Rust comments call out that simple multiplicative + // selectors do not appear as normal proof eval scalars. The Yul + // block mirrors that rule by accumulating those identities into + // SELECTOR_ACC_MPTR buckets for later multiplication by fixed + // selector commitments, while fully evaluated identities contribute + // to the negated expected scalar. + {%- include "partials/quotient_numerator/QuotientHelpers.yul" %} + {%- include "partials/quotient_numerator/QuotientNumeratorBlock.yul" %} + + // Return the compact output frame. Halo2Verifier checks the magic, + // stores word 1 as the linearization expected eval, then expands + // selector buckets into the fused final PCS MSM. + mstore(QUOTIENT_OUTPUT_MPTR, QUOTIENT_MAGIC) + mstore(add(QUOTIENT_OUTPUT_MPTR, 0x20), mload(QUOTIENT_EVAL_MPTR)) + {%- if simple_selector_cols.len() > 0 %} + // Copy selector buckets from the generated absolute memory region + // into the compact external-call return frame. + for { let q_i := 0 } lt(q_i, {{ simple_selector_cols.len() }}) { q_i := add(q_i, 1) } { + mstore(add(QUOTIENT_OUTPUT_MPTR, add(0x40, shl(5, q_i))), mload(add(SELECTOR_ACC_MPTR, shl(5, q_i)))) + } + {%- endif %} + return(QUOTIENT_OUTPUT_MPTR, QUOTIENT_OUTPUT_LEN) + } + } +} diff --git a/proofs/solidity-verifier/templates/contracts/Halo2Verifier.sol b/proofs/solidity-verifier/templates/contracts/Halo2Verifier.sol new file mode 100644 index 000000000..eea4edc23 --- /dev/null +++ b/proofs/solidity-verifier/templates/contracts/Halo2Verifier.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.24; + +/// @title Halo2 BLS12-381 KZG verifier. +/// @notice Circuit-specialized verifier for Midfall/midnight-proofs Halo2 +/// proofs rendered by this repository's Rust generator. +/// @dev This contract ports the verifier flow from +/// `midfall/proofs/src/plonk/verifier.rs`, the Keccak transcript comments from +/// `midfall/proofs/src/transcript/implementors.rs`, and the KZG multi-open +/// comments from `midfall/proofs/src/poly/kzg/mod.rs`. +/// @dev It is not a generic verifier. The proof layout, VK payload, quotient +/// identity program, memory layout, and optional quotient evaluator are all +/// generated for one `VerifyingKey>`. +/// +/// Halo2 KZG verifier for the BLS12-381 curve, midnight-proofs flavour. +/// +/// Differences vs the original BN254 / halo2 v0.4 template: +// +/// - BLS12-381 base field Fp is 381 bits and does not fit in a uint256. +/// Each Fp coord is encoded EIP-2537 padded (16 zero bytes + 48 bytes). +/// A G1 point is 128 bytes (4 words); a G2 point is 256 bytes (8). +/// - Calldata carries G1 commitments in uncompressed EIP-2537 padded +/// form (4 words = 128 bytes per point: x_hi, x_lo, y_hi, y_lo). The +/// proof bytes produced by midnight-proofs prover are repacked off +/// chain (compressed -> uncompressed) before being passed to +/// `verifyProof`. The verifier hashes the uncompressed 128-byte form into +/// the transcript verbatim, matching `Hashable for G1Projective::to_input`; +/// see `common_uncompressed_g1`. +/// - Transcript `common` absorbs raw inputs in order. `squeeze` computes one +/// Keccak digest, resets the transcript buffer to that digest, then samples +/// by interpreting the digest as a big-endian integer modulo r. +/// - Scalar inversion uses modexp(scalar, r-2, r). +/// - Constructors run a deployment-time smoke test for the EIP-2537 +/// precompiles using identity inputs. Compile with Solidity >=0.8.24 and +/// deploy only on chains/forks that support MCOPY and EIP-2537. +contract Halo2Verifier { + {% include "partials/verifier/Constants.sol" %} + + {% include "partials/verifier/PrecompileSmoke.sol" %} + + {% include "partials/verifier/Constructors.sol" %} + + /// @notice Verify a Halo2/Midfall proof for the generated verifying key. + /// @dev This checks only that `proof` verifies for the supplied public + /// `instances` under this pinned VK/protocol. Application contracts must + /// bind the meaning of those instances separately: state roots, program + /// identifiers, expected IVC outputs, chain/domain separation, and any + /// protocol-specific authorization are outside this raw verifier ABI. + /// @dev Production renders are success-or-revert: accepted proofs return + /// `true`, while malformed calldata, invalid proof material, failed + /// precompiles, or mismatched pinned dependency code revert. Trace and gas + /// renders keep the same failure policy. + /// @dev The generated verifier uses absolute Yul memory addresses instead + /// of Solidity's free-memory pointer, but generated scratch starts at + /// `0x80` so Solidity's reserved memory prefix is preserved. The main + /// assembly block remains terminal: accepted proofs return from assembly + /// and all rejected inputs revert. Do not inline this body into Solidity + /// code that continues executing after verification without reviewing the + /// memory strategy; see `docs/MEMORY_LAYOUT.md`. + /// @param proof Solidity-facing proof bytes, with G1 elements repacked into EIP-2537 padded uncompressed form. + /// @param instances Public instance scalars encoded as canonical BLS12-381 scalar-field words. + /// @return Always `true` for accepted proofs; invalid proofs revert instead of returning `false`. + function verifyProof( + bytes calldata proof, + uint256[] calldata instances + ) external {%- if self.trace || self.gas_checkpoints %} returns (bool) {%- else %} view returns (bool) {%- endif %} { + // Cheap ABI-shape guard before any generated memory work: + // - proof head must point at the bytes payload; + // - instances head must point at the generated instance array. + // + // The verifier below is a hand-rolled calldata parser. Failing here + // keeps malformed dynamic-argument layouts from being interpreted as a + // valid Midfall proof stream. + assembly ("memory-safe") { + if iszero(and(eq(calldataload({{ abi_selector_bytes|hex() }}), {{ abi_proof_head_offset|hex() }}), eq(calldataload({{ abi_instances_head_cptr|hex() }}), sub(NUM_INSTANCE_CPTR, {{ abi_selector_bytes|hex() }})))) { + revert(0, 0) + } + } + + {%- match self.embedded_vk %} + {%- when None %} + // Non-embedded renders pin the VK by address and codehash. The Yul + // loader rechecks the runtime before every proof and copies the + // INVALID-prefixed payload into VK_MPTR. + address vk = AUTHORIZED_VK; + {%- else %} + {%- endmatch %} + {%- match quotient_external %} + {%- when Some with (_) %} + // Split quotient renders delegate the scalar-side identity numerator + // reconstruction to a separately deployed generated evaluator. + address quotientEvaluator = AUTHORIZED_QUOTIENT; + {%- when None %} + {%- endmatch %} + assembly ("memory-safe") { + // This block owns the call-frame memory and remains terminal. + // Generated scratch starts at TRANSCRIPT_MPTR (0x80), preserving + // Solidity's reserved scratch, free-memory-pointer, and zero-slot + // words. See docs/MEMORY_LAYOUT.md. + // =============================================================== + // Helpers: modexp, transcript, EIP-2537 calls + // =============================================================== + + {% include "partials/verifier/AssemblyHelpers.yul" %} + + {% include "partials/verifier/AccumulatorHelpers.yul" %} + + {% include "partials/verifier/TraceAndGasHelpers.yul" %} + + let r := FR_MODULUS + let success := true + + {% include "partials/verifier/VkLoading.yul" %} + + {% include "partials/verifier/TranscriptProofParser.yul" %} + + {% include "partials/verifier/Lagrange.yul" %} + + {% include "partials/verifier/QuotientAndLinearization.yul" %} + + {% include "partials/verifier/Pcs.yul" %} + + {% include "partials/verifier/FinalPairing.yul" %} + + {% include "partials/verifier/TraceReturn.yul" %} + } + } +} diff --git a/proofs/solidity-verifier/templates/contracts/Halo2VerifyingKey.sol b/proofs/solidity-verifier/templates/contracts/Halo2VerifyingKey.sol new file mode 100644 index 000000000..955ae1072 --- /dev/null +++ b/proofs/solidity-verifier/templates/contracts/Halo2VerifyingKey.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.24; + +/// @title Halo2 BLS12-381 verifying-key payload. +/// @notice Contract whose deployed runtime is `INVALID || generated verifier-key payload`. +/// @dev Byte 0 is an unconditional INVALID opcode so direct calls cannot execute payload bytes as code. The linked verifier pins the full runtime by length/codehash and copies the payload starting at byte 1. +/// @dev The layout follows the verifier inputs derived from +/// `midfall/proofs/src/plonk/mod.rs::VerifyingKey` and the transcript +/// `vk.hash_into` behavior used by `midfall/proofs/src/plonk/verifier.rs`. +/// +/// Layout (in 32-byte words, big-endian). The header slots are generated from +/// Rust's `VkHeaderLayout`; the byte offsets are absolute from the start of the +/// VK payload, not from byte 0 of the runtime. Runtime byte 0 is the INVALID +/// prefix; the verifier loads the payload via +/// `extcodecopy(vk, VK_MPTR, 0x01, vk_payload_len)` and then references each +/// slot by `VK_MPTR + i`. +/// +/// word 0 : vk_digest (Fq, transcript_repr of the CS) +/// word 1 : num_instances +/// word 2 : k (log2 of the domain size) +/// word 3 : n_inv (1/n in Fr) +/// word 4 : omega (n-th primitive root of unity) +/// word 5 : omega_inv +/// word 6 : omega_inv_to_l (omega_inv ^ |rotation_last|) +/// word 7 : has_accumulator (0 or 1) +/// word 8 : acc_offset (instance index of the accumulator) +/// word 9 : num_acc_limbs +/// word 10 : num_acc_limb_bits +/// word 11..14 : G1_BASE (4 words, EIP-2537 padded) +/// word 15..22 : G2_BASE (8 words, EIP-2537 padded) +/// word 23..30 : NEG_S_G2_BASE (8 words, EIP-2537 padded) +/// word 31..30 + Q_PAYLOAD : quotient VM constants + packed bytecode +/// word 31 + Q_PAYLOAD .. : fixed_comms (4 words each) +/// word 31 + Q_PAYLOAD + 4*N_FIXED .. +/// : permutation_comms (4 words each) +/// +/// Notes: +/// - `extcodehash` of this contract is pinned by the linked verifier via +/// `EXPECTED_VK_CODEHASH`, so any byte tweak is detected at deploy time. +/// - The quotient identity interpreter's static program is stored in this +/// pinned VK runtime. The verifier reads it from memory after `extcodecopy`, +/// avoiding verifier-side PUSH32/mstore immediates while keeping the program +/// covered by `EXPECTED_VK_CODEHASH`. +/// - The midnight-proofs migration bakes the per-lookup chunk counts, trashcan +/// structure, and `num_simple_selectors` into the generated verifier code. +contract Halo2VerifyingKey { + /// @notice Deploy the verifying-key payload as this contract's runtime bytecode. + /// @dev The constructor writes an INVALID byte followed by generated words into memory and returns that prefixed runtime. + /// @dev The transient construction buffer starts at `0x80`, preserving Solidity's reserved memory words. + constructor() { + assembly { + // Runtime layout: + // byte 0 : INVALID, so the payload cannot be executed + // byte 1..end : generated VK payload copied by Halo2Verifier + // + // `runtime` includes the INVALID prefix; `payload` points to word + // zero of the verifier-key data described in the contract NatSpec. + let runtime := {{ constructor_payload_mptr|hex() }} + let payload := add(runtime, 0x01) + mstore8(runtime, 0xfe) + // Header, base-point, and quotient-program words generated from + // VkPayloadLayout. The inline names on each mstore identify the + // exact slot in the rendered source. + {%- for (name, chunk) in constants %} + mstore(add(payload, {{ (32 * loop.index0)|hex_padded(4) }}), {{ chunk|hex_padded(64) }}) // {{ name }} + {%- endfor %} + {%- for (x_hi, x_lo, y_hi, y_lo) in fixed_comms %} + {%- let offset = constants.len() %} + // Fixed-column commitment {{ loop.index0 }}, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, {{ (32 * (offset + 4 * loop.index0))|hex_padded(4) }}), {{ x_hi|hex_padded(64) }}) // fixed_comms[{{ loop.index0 }}].x_hi + mstore(add(payload, {{ (32 * (offset + 4 * loop.index0 + 1))|hex_padded(4) }}), {{ x_lo|hex_padded(64) }}) // fixed_comms[{{ loop.index0 }}].x_lo + mstore(add(payload, {{ (32 * (offset + 4 * loop.index0 + 2))|hex_padded(4) }}), {{ y_hi|hex_padded(64) }}) // fixed_comms[{{ loop.index0 }}].y_hi + mstore(add(payload, {{ (32 * (offset + 4 * loop.index0 + 3))|hex_padded(4) }}), {{ y_lo|hex_padded(64) }}) // fixed_comms[{{ loop.index0 }}].y_lo + {%- endfor %} + {%- for (x_hi, x_lo, y_hi, y_lo) in permutation_comms %} + {%- let offset = constants.len() + 4 * fixed_comms.len() %} + // Permutation commitment {{ loop.index0 }}, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, {{ (32 * (offset + 4 * loop.index0))|hex_padded(4) }}), {{ x_hi|hex_padded(64) }}) // permutation_comms[{{ loop.index0 }}].x_hi + mstore(add(payload, {{ (32 * (offset + 4 * loop.index0 + 1))|hex_padded(4) }}), {{ x_lo|hex_padded(64) }}) // permutation_comms[{{ loop.index0 }}].x_lo + mstore(add(payload, {{ (32 * (offset + 4 * loop.index0 + 2))|hex_padded(4) }}), {{ y_hi|hex_padded(64) }}) // permutation_comms[{{ loop.index0 }}].y_hi + mstore(add(payload, {{ (32 * (offset + 4 * loop.index0 + 3))|hex_padded(4) }}), {{ y_lo|hex_padded(64) }}) // permutation_comms[{{ loop.index0 }}].y_lo + {%- endfor %} + + // Return exactly the INVALID prefix plus the generated payload. The + // linked verifier pins this byte length and the resulting codehash. + return(runtime, {{ (1 + 32 * (constants.len() + 4 * fixed_comms.len() + 4 * permutation_comms.len()))|hex() }}) + } + } +} diff --git a/proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientHelpers.yul b/proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientHelpers.yul new file mode 100644 index 000000000..a9ec4d9e7 --- /dev/null +++ b/proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientHelpers.yul @@ -0,0 +1,71 @@ + // Optional quotient helper functions. Each one is rendered only + // when the Rust lowering pass recognized the corresponding + // expression shape in this generated verifier. They are pure Fr + // helpers and share the same FR_MODULUS as the surrounding + // numerator block. + {%- if self.quotient_pow5_helper %} + // VK-specialized identity helper for Poseidon S-box terms. + // + // Rust source shape: + // circuits/src/hash/poseidon/poseidon_chip.rs::sbox + // full_round_gate / partial_round_gate + // circuits/src/hash/poseidon/round_skips.rs::RoundId + // + // The Rust verifier only sees this as an Expression tree from + // `vk.cs.gates`; the generator emits q_pow5 after recognizing five + // equal multiplicative factors. It is a codegen shortcut for x^5, + // not a separate verifier rule. + function q_pow5(x) -> z { + let q_r := FR_MODULUS + let x2 := mulmod(x, x, q_r) + z := mulmod(x, mulmod(x2, x2, q_r), q_r) + } + + {%- endif %} + {%- if self.quotient_limb7_helper %} + // Compact evaluator for a recurring 7-limb foreign-field linear + // combination. + // + // Rust source shape: + // circuits/src/field/foreign/params.rs::base_powers + // foreign/gates/norm.rs::Foreign-field normalization + // foreign/gates/mul.rs::Foreign-field multiplication + // + // This evaluates sum_i base_powers[i] * limb_i over Fr for this + // generated 7-limb basis. It is used for normalization and the + // sum_x/sum_y/sum_z pieces of multiplication. + function q_limb7(x0, x1, x2, x3, x4, x5, x6) -> z { + let q_r := FR_MODULUS + {%- for coeff in limb7_yul_coeffs %} + {%- if loop.first %} + z := addmod(x0, mulmod({{ coeff }}, x{{ loop.index }}, q_r), q_r) + {%- else %} + z := addmod(z, mulmod({{ coeff }}, x{{ loop.index }}, q_r), q_r) + {%- endif %} + {%- endfor %} + } + + {%- endif %} + {%- if self.quotient_wide_limb7_helper %} + // Wide helper for the pairwise-product side of foreign-field + // multiplication. + // + // Rust source shape: + // foreign/gates/mul.rs::Foreign-field multiplication + // xys = pair_wise_prod(xs, ys) + // sum_exprs(double_base_powers, xys) + // + // Native multiplication callbacks group repeated 7-term slices of + // the double-base product-convolution basis into q_limb7_wide. + function q_limb7_wide(x0, x1, x2, x3, x4, x5, x6) -> z { + let q_r := FR_MODULUS + {%- for coeff in wide_limb7_yul_coeffs %} + {%- if loop.first %} + z := addmod(x0, mulmod({{ coeff }}, x{{ loop.index }}, q_r), q_r) + {%- else %} + z := addmod(z, mulmod({{ coeff }}, x{{ loop.index }}, q_r), q_r) + {%- endif %} + {%- endfor %} + } + + {%- endif %} diff --git a/proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientNumeratorBlock.yul b/proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientNumeratorBlock.yul new file mode 100644 index 000000000..8207e6571 --- /dev/null +++ b/proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientNumeratorBlock.yul @@ -0,0 +1,1269 @@ + // =============================================================== + // Batched identity numerator / linearization target. + // + // This block does not evaluate the quotient polynomial h(x), and + // the proof does not provide an h(x) scalar to trust. Instead it: + // + // 1. Reconstructs the y-batched constraint numerator nu_y(x) + // from the alleged polynomial evaluations read after the + // transcript sampled x. + // 2. Stores -nu_y(x) as the expected opening scalar for the + // linearized commitment. + // + // The commitment side is built in the next block from the quotient + // limb commitments as (1 - x^n) * Σ_i x_split^i * Q_i, plus any + // simple-selector commitments. The PCS check later binds that + // linearized commitment to this expected scalar at x. + // + // Rust source-of-truth: + // - verifier.rs reads quotient commitments, samples x, then + // reads/computes all evaluations used below. + // - mod.rs::partially_evaluate_identities returns identities in + // gate, permutation, lookup, trash order. + // - linearization/verifier.rs::compute_linearization_commitment + // reverse-folds those identities by powers of y, sends + // simple-selector identities to selector commitment scalars, + // and subtracts fully-evaluated identities into expected_eval. + // + // This template is shared by the monolithic and external quotient + // paths. In the external path, Halo2QuotientEvaluator first copies + // the verifier memory frame into the same generated addresses. + // + // Runtime inputs expected to exist before this block starts: + // - `r` is the BLS12-381 scalar-field modulus. + // - Y_MPTR holds the quotient batching challenge y. + // - X_MPTR, L_*_MPTR, INSTANCE_EVAL_MPTR, and + // REVERSED_EVALS_MPTR hold values parsed or derived by the + // main verifier after the transcript sampled x. + // - VK_MPTR holds the pinned VK payload; in compact mode that + // payload includes the quotient constant table and bytecode. + // + // Runtime outputs written by this block: + // - QUOTIENT_EVAL_MPTR receives the scalar expected opening for + // the linearized commitment, namely -nu_y(x). + // - SELECTOR_ACC_MPTR[0..num_simple_selectors) receives one + // linearization scalar per generated simple selector. + // + // Line-by-line reading conventions used below: + // + // * Every runtime value is one canonical Fr element stored in a + // 256-bit EVM memory word. The small integer operands decoded + // from q_program are never field values; they are pointers, + // constant-table slots, selector indexes, offsets, or counts. + // + // * `mload(ptr)` is the only way the VM turns a small pointer + // operand into a real 255-bit field element. The value loaded + // from memory is then combined with `addmod(..., r)` or + // `mulmod(..., r)`, so every arithmetic line is reduced modulo + // the BLS12-381 scalar-field order. + // + // * `q_top` is the cached top of the VM operand stack. When an + // opcode needs to push while `q_top` is already live, the old + // value is written to `q_sp` and `q_sp` is advanced by one + // word. Binary `ADD`/`MUL` move `q_sp` back by one word and + // combine that spilled value with `q_top`. + // + // * Identity boundaries are explicit. Expression opcodes leave + // one value in `q_top`; `FOLD_MAIN` or `FOLD_SELECTOR` consumes + // it and advances the global y-batch position. Native callback + // opcodes are only emitted at empty-stack boundaries and run + // generated Yul that performs the same fold side effects. + // + // * The generated Solidity source intentionally emits comments + // before opcode cases. Those comments are documentation only: + // they do not affect bytecode, but they make rendered verifier + // assembly readable without jumping back to Rust codegen. + // =============================================================== + { + {%- match quotient_program %} + {%- when Some with (program) %} + // Compact quotient-program mode. + // + // The largest identity expressions are not all emitted as + // unrolled Yul. Instead, most arithmetic is encoded as a small + // q_program bytecode stored in the VK payload. This block + // interprets that program, while selected heavy identities may + // still be emitted as native callbacks for gas. + // + // Compact mode is a code-size trade: short bytecode operands + // name already-planned memory slots, and the interpreter turns + // those names into Fr arithmetic. The opcode stream is fully + // generated and pinned by the VK/runtime codehash; no proof + // calldata can alter control flow. + // Load the quotient batching challenge used by every fold. + let y := mload(Y_MPTR) + + // q_const_mptr points to Fr constants used by the VM. + // q_program_mptr points to the bytecode stream. + // Constants are stored as consecutive 32-byte Fr words. + let q_const_mptr := {{ program.const_mptr|hex() }} + // Program bytes are also stored in the VK payload, packed into + // 32-byte words by PackedProgramCodec. + let q_program_mptr := {{ program.program_mptr|hex() }} + // Running Horner accumulator for fully evaluated identities. + // After all identities, this is nu_y(x) for the `None` + // identity group. + // Initialize A = 0 before scanning the identity stream. + mstore({{ program.eval_numer_mptr|hex() }}, 0) + {%- if self.trace %} + // Trace builds label each emitted identity in y-batch order. + mstore({{ program.trace_id_mptr|hex() }}, {{ quotient_identity_trace_base }}) + {%- endif %} + {%- if simple_selector_cols.len() > 0 %} + // Simple selectors are grouped into separate linearization + // buckets. They start at zero for every proof. + // q_sel_zero_off walks selector bucket byte offsets. + for { let q_sel_zero_off := 0 } lt(q_sel_zero_off, {{ (simple_selector_cols.len() * 0x20)|hex() }}) { q_sel_zero_off := add(q_sel_zero_off, 0x20) } { + // B_s = 0 for each simple selector bucket. + mstore(add(SELECTOR_ACC_MPTR, q_sel_zero_off), 0) + } + + {%- if program.selector_max_power > 0 %} + // Codegen knows the selector identity positions. Precompute + // the y^k powers needed for selector gap and tail updates, + // avoiding a runtime y^-1 modexp and per-identity selector + // scale maintenance. + { + // q_y_power holds y^i at the current loop index. + let q_y_power := 1 + // Start at i=1 because y^0 = 1 is implicit and never read. + for { let q_y_power_i := 1 } lt(q_y_power_i, {{ program.selector_max_power + 1 }}) { q_y_power_i := add(q_y_power_i, 1) } { + // Advance from y^(i-1) to y^i modulo Fr. + q_y_power := mulmod(q_y_power, y, r) + // Store y^i at selector_power_mptr + 32*i. + mstore(add({{ program.selector_power_mptr|hex() }}, shl(5, q_y_power_i)), q_y_power) + } + } + {%- endif %} + {%- endif %} + + // Direct inline prefix. These identities are generated as Yul + // before entering the VM. They use the same fold snippets as + // VM/native identities, so they occupy the same y-batch order. + {%- for code_block in quotient_inline_computations %} + {%- for line in code_block %} + {{ line }} + {%- endfor %} + {%- endfor %} + + // VM registers: + // q_pc current bytecode pointer + // q_end end of bytecode stream + // q_sp memory stack pointer for non-top stack values + // q_top cached top-of-stack value + // q_has_top whether q_top currently holds a stack value + // + // The cached top reduces memory traffic in the interpreter. + // q_sp's registered range must cover the interpreted operand + // stack plus any native callback scratch that reuses this base + // pointer. In particular, the native permutation callback + // writes a structured scratch table at program.stack_mptr. + // q_pc starts at the first encoded instruction. + let q_pc := q_program_mptr + // q_end is an exclusive byte pointer for the VM loop. + let q_end := add(q_program_mptr, {{ program.len|hex() }}) + // q_sp starts at the first free stack word. + let q_sp := {{ program.stack_mptr|hex() }} + // q_top is meaningless until q_has_top is set. + let q_top := 0 + // q_has_top = 0 means the VM stack is empty. + let q_has_top := 0 + + // q_program opcode summary: + // 0x01/0x09 push const 0x02/0x05 push memory + // 0x03/0x04 push token ptr 0x06 add, 0x07 mul, 0x08 neg + // 0x0a fold main identity 0x0b fold selector identity + // 0x0c..0x11 add/mul const or memory into top + // 0x12..0x16 fused add-mul runs + // 0x17/0x18 reserved + // 0x19 native permutation 0x1b native heavy identity + // 0x1c LIN7 0x1d BILIN7_ROW + // 0x1e BILIN7_PAIRWISE 0x1f native lookup + // 0x20 POW5 0x21 MODARITH7 + // 0x22 AFFINE_SUM + // + // The default IVC verifier uses one physical encoding for the + // logical VM: compact byte-oriented opcodes with variable-width + // operands, dynamic runs, and limb-aware cases. + {# + Template-only switch/case reference. + + This comment documents the interpreter source without being + emitted into generated Solidity. Runtime behavior lives in the + switch blocks below; opcode numbers and operand layouts are + defined in src/codegen/quotient/mod.rs. + + Shared stack model: + - q_top caches the top stack value. + - q_has_top records whether q_top is live. + - q_sp points just past spilled stack words. + - push-like cases spill the old q_top when q_has_top is set. + - ADD/MUL pop by moving q_sp back one word. + - accumulator cases mutate q_top in place. + - FOLD_* cases consume q_top and advance the y-batch state. + - native callbacks reset q_top/q_sp and perform their own + fold calls inside generated callback code. + + Case map: + 0x01 push_const + bytes: u16 const_idx + effect: push q_const_mptr[const_idx] + + 0x02 push_mem_literal + bytes: u32 ptr + effect: push mload(ptr) + + 0x03 push_mem_token + bytes: u8 token + effect: resolve token to a generated pointer and push it + + 0x04 push_mem_token_offset + bytes: u8 token, u32 offset + effect: resolve token, add byte offset, and push mload + + 0x05 push_mem_u16 + bytes: u16 ptr + effect: compact pointer load + + 0x06 add + effect: q_top = stack_pop() + q_top mod Fr + + 0x07 mul + effect: q_top = stack_pop() * q_top mod Fr + + 0x08 neg + effect: q_top = -q_top mod Fr + + 0x09 push_const_u8 + bytes: u8 const_idx + effect: short form of push_const + + 0x0a fold_main + effect: trace q_top, advance q_eval_numer = q_eval_numer*y + + q_top + + 0x0b fold_selector + bytes: u8 selector_idx, u16 selector_gap + effect: trace q_top, advance the global y position, and add + q_top into a selector bucket after applying its + codegen-known y^gap + + 0x0c add_const_u8 + effect: q_top += q_const_mptr[u8 const_idx] + + 0x0d mul_const_u8 + effect: q_top *= q_const_mptr[u8 const_idx] + + 0x0e add_const + effect: q_top += q_const_mptr[u16 const_idx] + + 0x0f mul_const + effect: q_top *= q_const_mptr[u16 const_idx] + + 0x10 add_mem_u16 + effect: q_top += mload(u16 ptr) + + 0x11 mul_mem_u16 + effect: q_top *= mload(u16 ptr) + + 0x12 add_mul_mem_mem_const_u8 + bytes: u16 lhs, u16 rhs, u8 const_idx + effect: q_top += mload(lhs) * mload(rhs) * const + + 0x13 add_mul_const_u8_mem_u16 + bytes: u16 ptr, u8 const_idx + effect: q_top += mload(ptr) * const + + 0x14 add_mul_mem_mem + bytes: u16 lhs, u16 rhs + effect: q_top += mload(lhs) * mload(rhs) + + 0x15 run_add_mul_mem_mem_const_u8 + bytes: u16 count, then count * {u16 lhs, u16 rhs, u8 c} + effect: repeated 0x12 in one dispatch + + 0x16 run_add_mul_const_u8_mem_u16 + bytes: u16 count, then count * {u16 ptr, u8 c} + effect: repeated 0x13 in one dispatch + + 0x22 affine_sum + bytes: u16 lin_count, u16 product_count, + lin_count * {u16 ptr, u8 c}, + product_count * {u16 lhs, u16 rhs, u8 c} + effect: q_top += sum c_i * mload(p_i) + + sum c_i * mload(p_i) * mload(q_i) + + 0x19 native_permutation + effect: evaluate and fold the generated permutation identity + family at this VM stream position + + 0x1a reserved + effect: no switch case; default branch reverts + + 0x1b native_identity + effect: dispatch to one generated heavy-gate callback by index + + 0x1c lin7 + bytes: seven {u8 const_idx, u16 ptr} pairs + effect: push sum_i const_i * mload(ptr_i) + + 0x1d bilin7_row + bytes: u16 lhs, seven {u8 const_idx, u16 rhs} pairs + effect: push mload(lhs) * sum_i const_i * mload(rhs_i) + + 0x1e bilin7_pairwise + bytes: u16 lhs_base, u16 rhs_base, 13 const indexes + effect: push sum_{i,j} const_{i+j} * lhs_i * rhs_j + + 0x1f native_lookup + effect: evaluate and fold LogUp boundary/helper/accumulator + identities at this VM stream position + + 0x20 pow5 + effect: q_top = q_top^5 + + 0x21 modarith7 + bytes: flags, optional cond/constant, count header, + then fused LIN7/BILIN7/mem/product blocks + effect: push one fused affine 7-limb identity value + + Memory token map used by 0x03/0x04: + 0x01 L_0_MPTR + 0x02 L_LAST_MPTR + 0x03 L_BLIND_MPTR + 0x04 BETA_MPTR + 0x05 GAMMA_MPTR + 0x06 X_MPTR + 0x07 THETA_MPTR + 0x08 TRASH_CHALLENGE_MPTR + 0x09 INSTANCE_EVAL_MPTR + #} + // Byte-oriented encoding: opcodes are one byte followed by + // variable-width operand bytes. + for { } lt(q_pc, q_end) { } { + // The bytecode table is byte-addressed, but EVM memory + // loads whole words. `byte(0, mload(q_pc))` extracts the + // opcode at the current byte cursor; each case advances + // q_pc by exactly its operand width. + let q_op := byte(0, mload(q_pc)) + q_pc := add(q_pc, 1) + + switch q_op + {%- if program.op_usage.push_const %} + // VM 0x01 PUSH_CONST (bytes): next two bytes are a constant-table slot. + case {{ template_constants.quotient_vm.op.push_const|hex() }} { + // Operand layout: u16 const_idx. Constants are 32-byte + // Fr words, so shl(5, const_idx) converts an index to + // a byte offset. + let qconst := shr(240, mload(q_pc)) + // Push semantics: spill the old cached top, if any, + // then install the loaded constant as the new q_top. + if q_has_top { + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } + q_top := mload(add(q_const_mptr, shl(5, qconst))) + q_has_top := 1 + q_pc := add(q_pc, 2) + } + {%- endif %} + {%- if program.op_usage.push_mem_literal %} + // VM 0x02 PUSH_MEM_LITERAL (bytes): next four bytes are a memory pointer. + case {{ template_constants.quotient_vm.op.push_mem_literal|hex() }} { + // Operand layout: u32 absolute memory pointer. This is + // emitted only for planned addresses that do not fit in + // the smaller u16 form or token map. + let q_ptr := shr(224, mload(q_pc)) + q_pc := add(q_pc, {{ template_constants.quotient_vm.byte_u32_bytes|hex() }}) + // Load one canonical Fr word from generated memory and + // push it through the cached-top stack discipline. + if q_has_top { + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } + q_top := mload(q_ptr) + q_has_top := 1 + } + {%- endif %} + {%- if program.op_usage.push_mem_token %} + // VM 0x03 PUSH_MEM_TOKEN (bytes): next byte selects a symbolic memory pointer. + case {{ template_constants.quotient_vm.op.push_mem_token|hex() }} { + // Operand layout: u8 token. Tokens cover the hot + // global verifier scalars used in many identities. + let q_token := byte(0, mload(q_pc)) + q_pc := add(q_pc, 1) + let q_ptr := 0 + // Token cases decode generated symbolic memory addresses. + switch q_token + {%- if program.mem_usage.l0 %} + case {{ template_constants.quotient_vm.mem.l0|hex() }} { q_ptr := L_0_MPTR } + {%- endif %} + {%- if program.mem_usage.l_last %} + case {{ template_constants.quotient_vm.mem.l_last|hex() }} { q_ptr := L_LAST_MPTR } + {%- endif %} + {%- if program.mem_usage.l_blind %} + case {{ template_constants.quotient_vm.mem.l_blind|hex() }} { q_ptr := L_BLIND_MPTR } + {%- endif %} + {%- if program.mem_usage.beta %} + case {{ template_constants.quotient_vm.mem.beta|hex() }} { q_ptr := BETA_MPTR } + {%- endif %} + {%- if program.mem_usage.gamma %} + case {{ template_constants.quotient_vm.mem.gamma|hex() }} { q_ptr := GAMMA_MPTR } + {%- endif %} + {%- if program.mem_usage.x %} + case {{ template_constants.quotient_vm.mem.x|hex() }} { q_ptr := X_MPTR } + {%- endif %} + {%- if program.mem_usage.theta %} + case {{ template_constants.quotient_vm.mem.theta|hex() }} { q_ptr := THETA_MPTR } + {%- endif %} + {%- if program.mem_usage.trash_challenge %} + case {{ template_constants.quotient_vm.mem.trash_challenge|hex() }} { q_ptr := TRASH_CHALLENGE_MPTR } + {%- endif %} + {%- if program.mem_usage.instance_eval %} + case {{ template_constants.quotient_vm.mem.instance_eval|hex() }} { q_ptr := INSTANCE_EVAL_MPTR } + {%- endif %} + // A token not advertised by the VM usage manifest is + // impossible for valid generated bytecode. + default { revert(0, 0) } + if q_has_top { + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } + q_top := mload(q_ptr) + q_has_top := 1 + } + {%- endif %} + {%- if program.op_usage.push_mem_token_offset %} + // VM 0x04 PUSH_MEM_TOKEN_OFFSET (bytes): token byte plus u32 byte offset. + case {{ template_constants.quotient_vm.op.push_mem_token_offset|hex() }} { + // Operand layout: u8 token, u32 byte offset. This is + // used when a symbolic base such as REVERSED_EVALS_MPTR + // is known, but the expression needs a specific word + // inside that generated region. + let q_word := mload(q_pc) + let q_token := byte(0, q_word) + let q_off := and(shr(216, q_word), 0xffffffff) + q_pc := add(q_pc, 5) + let q_ptr := 0 + // Token-offset cases reuse the same token table, then add q_off. + switch q_token + {%- if program.mem_usage.l0 %} + case {{ template_constants.quotient_vm.mem.l0|hex() }} { q_ptr := add(L_0_MPTR, q_off) } + {%- endif %} + {%- if program.mem_usage.l_last %} + case {{ template_constants.quotient_vm.mem.l_last|hex() }} { q_ptr := add(L_LAST_MPTR, q_off) } + {%- endif %} + {%- if program.mem_usage.l_blind %} + case {{ template_constants.quotient_vm.mem.l_blind|hex() }} { q_ptr := add(L_BLIND_MPTR, q_off) } + {%- endif %} + {%- if program.mem_usage.beta %} + case {{ template_constants.quotient_vm.mem.beta|hex() }} { q_ptr := add(BETA_MPTR, q_off) } + {%- endif %} + {%- if program.mem_usage.gamma %} + case {{ template_constants.quotient_vm.mem.gamma|hex() }} { q_ptr := add(GAMMA_MPTR, q_off) } + {%- endif %} + {%- if program.mem_usage.x %} + case {{ template_constants.quotient_vm.mem.x|hex() }} { q_ptr := add(X_MPTR, q_off) } + {%- endif %} + {%- if program.mem_usage.theta %} + case {{ template_constants.quotient_vm.mem.theta|hex() }} { q_ptr := add(THETA_MPTR, q_off) } + {%- endif %} + {%- if program.mem_usage.trash_challenge %} + case {{ template_constants.quotient_vm.mem.trash_challenge|hex() }} { q_ptr := add(TRASH_CHALLENGE_MPTR, q_off) } + {%- endif %} + {%- if program.mem_usage.instance_eval %} + case {{ template_constants.quotient_vm.mem.instance_eval|hex() }} { q_ptr := add(INSTANCE_EVAL_MPTR, q_off) } + {%- endif %} + default { revert(0, 0) } + if q_has_top { + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } + q_top := mload(q_ptr) + q_has_top := 1 + } + {%- endif %} + {%- if program.op_usage.push_mem_u16 %} + // VM 0x05 PUSH_MEM_U16 (bytes): next two bytes are a short memory pointer. + case {{ template_constants.quotient_vm.op.push_mem_u16|hex() }} { + // Operand layout: u16 absolute memory pointer. The + // memory planner keeps the hot quotient frame below + // 64 KiB when this compact form is emitted. + let q_ptr := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + if q_has_top { + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } + q_top := mload(q_ptr) + q_has_top := 1 + } + {%- endif %} + {%- if program.op_usage.add %} + // VM 0x06 ADD: pop one spilled stack word and add it to q_top. + case {{ template_constants.quotient_vm.op.add|hex() }} { + // The safety validator guarantees a spilled operand + // exists before ADD. q_top is the right operand. + q_sp := sub(q_sp, 0x20) + q_top := addmod(mload(q_sp), q_top, r) + } + {%- endif %} + {%- if program.op_usage.mul %} + // VM 0x07 MUL: pop one spilled stack word and multiply it by q_top. + case {{ template_constants.quotient_vm.op.mul|hex() }} { + // Same stack contract as ADD, with multiplication + // reduced directly modulo Fr. + q_sp := sub(q_sp, 0x20) + q_top := mulmod(mload(q_sp), q_top, r) + } + {%- endif %} + {%- if program.op_usage.neg %} + // VM 0x08 NEG: replace q_top with its Fr negation. + case {{ template_constants.quotient_vm.op.neg|hex() }} { + // addmod(0, r - x, r) maps zero back to zero and every + // nonzero scalar to its canonical additive inverse. + q_top := addmod(0, sub(r, q_top), r) + } + {%- endif %} + {%- if program.op_usage.pow5 %} + // VM 0x20 POW5: replace q_top with q_top^5. + case {{ template_constants.quotient_vm.op.pow5|hex() }} { + // Poseidon S-box shortcut: x^5 = x * (x^2)^2. + let q2 := mulmod(q_top, q_top, r) + q_top := mulmod(q_top, mulmod(q2, q2, r), r) + } + {%- endif %} + {%- if program.op_usage.push_const_u8 %} + // VM 0x09 PUSH_CONST_U8 (bytes): next byte is a constant-table slot. + case {{ template_constants.quotient_vm.op.push_const_u8|hex() }} { + // One-byte variant for the common case where the + // constant table has fewer than 256 referenced slots. + let qconst := byte(0, mload(q_pc)) + if q_has_top { + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } + q_top := mload(add(q_const_mptr, shl(5, qconst))) + q_has_top := 1 + q_pc := add(q_pc, 1) + } + {%- endif %} + {%- if program.op_usage.add_const_u8 %} + // VM 0x0c ADD_CONST_U8: add a small constant-table slot into q_top. + case {{ template_constants.quotient_vm.op.add_const_u8|hex() }} { + // Accumulator opcode: mutates q_top in place instead + // of pushing a new stack value. + let qconst := byte(0, mload(q_pc)) + q_pc := add(q_pc, 1) + q_top := addmod(q_top, mload(add(q_const_mptr, shl(5, qconst))), r) + } + {%- endif %} + {%- if program.op_usage.mul_const_u8 %} + // VM 0x0d MUL_CONST_U8: multiply q_top by a small constant-table slot. + case {{ template_constants.quotient_vm.op.mul_const_u8|hex() }} { + // One-byte constant-index multiply, used by short + // affine chains after an initial PUSH. + let qconst := byte(0, mload(q_pc)) + q_pc := add(q_pc, 1) + q_top := mulmod(q_top, mload(add(q_const_mptr, shl(5, qconst))), r) + } + {%- endif %} + {%- if program.op_usage.add_const %} + // VM 0x0e ADD_CONST: add a wider constant-table slot into q_top. + case {{ template_constants.quotient_vm.op.add_const|hex() }} { + // Two-byte constant-index form for larger generated + // constant tables. + let qconst := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + q_top := addmod(q_top, mload(add(q_const_mptr, shl(5, qconst))), r) + } + {%- endif %} + {%- if program.op_usage.mul_const %} + // VM 0x0f MUL_CONST: multiply q_top by a wider constant-table slot. + case {{ template_constants.quotient_vm.op.mul_const|hex() }} { + // Two-byte constant-index multiply. + let qconst := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + q_top := mulmod(q_top, mload(add(q_const_mptr, shl(5, qconst))), r) + } + {%- endif %} + {%- if program.op_usage.add_mem_u16 %} + // VM 0x10 ADD_MEM_U16: add a short memory load into q_top. + case {{ template_constants.quotient_vm.op.add_mem_u16|hex() }} { + // Operand layout: u16 pointer. The pointed word is an + // already range-checked Fr scalar in verifier memory. + let q_ptr := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + q_top := addmod(q_top, mload(q_ptr), r) + } + {%- endif %} + {%- if program.op_usage.mul_mem_u16 %} + // VM 0x11 MUL_MEM_U16: multiply q_top by a short memory load. + case {{ template_constants.quotient_vm.op.mul_mem_u16|hex() }} { + // In-place multiply by a planned memory word. + let q_ptr := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + q_top := mulmod(q_top, mload(q_ptr), r) + } + {%- endif %} + {%- if program.op_usage.add_mul_mem_mem_const_u8 %} + // VM 0x12 ADD_MUL_MEM_MEM_CONST_U8: fused q_top += lhs * rhs * const. + case {{ template_constants.quotient_vm.op.add_mul_mem_mem_const_u8|hex() }} { + // Operand layout: u16 lhs_ptr, u16 rhs_ptr, + // u8 const_idx. This fuses a frequent affine-product + // term without spending opcodes on PUSH/MUL/MUL/ADD. + let q_word := mload(q_pc) + let q_lhs := shr(240, q_word) + let q_rhs := and(shr(224, q_word), 0xffff) + let qconst := byte(4, q_word) + q_pc := add(q_pc, 5) + q_top := addmod( + q_top, + mulmod( + mulmod(mload(q_lhs), mload(q_rhs), r), + mload(add(q_const_mptr, shl(5, qconst))), + r + ), + r + ) + } + {%- endif %} + {%- if program.op_usage.add_mul_const_u8_mem_u16 %} + // VM 0x13 ADD_MUL_CONST_U8_MEM_U16: fused q_top += mem * const. + case {{ template_constants.quotient_vm.op.add_mul_const_u8_mem_u16|hex() }} { + // Operand layout: u16 ptr, u8 const_idx. + let q_word := mload(q_pc) + let q_ptr := shr(240, q_word) + let qconst := byte(2, q_word) + q_pc := add(q_pc, 3) + q_top := addmod( + q_top, + mulmod(mload(q_ptr), mload(add(q_const_mptr, shl(5, qconst))), r), + r + ) + } + {%- endif %} + {%- if program.op_usage.add_mul_mem_mem %} + // VM 0x14 ADD_MUL_MEM_MEM: fused q_top += lhs * rhs. + case {{ template_constants.quotient_vm.op.add_mul_mem_mem|hex() }} { + // Operand layout: u16 lhs_ptr, u16 rhs_ptr. This is + // the unit-coefficient sibling of opcode 0x12. + let q_word := mload(q_pc) + let q_lhs := shr(240, q_word) + let q_rhs := and(shr(224, q_word), 0xffff) + q_pc := add(q_pc, {{ template_constants.quotient_vm.byte_u32_bytes|hex() }}) + q_top := addmod(q_top, mulmod(mload(q_lhs), mload(q_rhs), r), r) + } + {%- endif %} + {%- if program.op_usage.run_add_mul_mem_mem_const_u8 %} + // VM 0x15 RUN_ADD_MUL_MEM_MEM_CONST_U8: byte-only loop over fused 0x12 payloads. + case {{ template_constants.quotient_vm.op.run_add_mul_mem_mem_const_u8|hex() }} { + // Operand layout: u16 count followed by `count` + // packed 0x12-style payloads. The run header saves a + // dispatch per term in long affine product chains. + let q_count := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + let q_run_end := add(q_pc, mul(q_count, 5)) + for { } lt(q_pc, q_run_end) { } { + let q_word := mload(q_pc) + let q_lhs := shr(240, q_word) + let q_rhs := and(shr(224, q_word), 0xffff) + let qconst := byte(4, q_word) + q_pc := add(q_pc, 5) + q_top := addmod( + q_top, + mulmod( + mulmod(mload(q_lhs), mload(q_rhs), r), + mload(add(q_const_mptr, shl(5, qconst))), + r + ), + r + ) + } + } + {%- endif %} + {%- if program.op_usage.run_add_mul_const_u8_mem_u16 %} + // VM 0x16 RUN_ADD_MUL_CONST_U8_MEM_U16: byte-only loop over fused 0x13 payloads. + case {{ template_constants.quotient_vm.op.run_add_mul_const_u8_mem_u16|hex() }} { + // Operand layout: u16 count followed by `count` + // packed 0x13-style payloads. + let q_count := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + let q_run_end := add(q_pc, mul(q_count, 3)) + for { } lt(q_pc, q_run_end) { } { + let q_word := mload(q_pc) + let q_ptr := shr(240, q_word) + let qconst := byte(2, q_word) + q_pc := add(q_pc, 3) + q_top := addmod( + q_top, + mulmod(mload(q_ptr), mload(add(q_const_mptr, shl(5, qconst))), r), + r + ) + } + } + {%- endif %} + {%- if program.op_usage.affine_sum %} + // VM 0x22 AFFINE_SUM: mixed byte-only affine linear/product loop. + case {{ template_constants.quotient_vm.op.affine_sum|hex() }} { + // Operand layout: + // u16 lin_count, u16 product_count, + // lin_count * {u16 ptr, u8 const_idx}, + // product_count * {u16 lhs, u16 rhs, u8 const_idx}. + // The opcode appends all terms into the existing + // q_top accumulator. + let q_lin_count := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + let q_product_count := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + // Linear part: q_top += c_i * mload(ptr_i). + let q_lin_end := add(q_pc, mul(q_lin_count, 3)) + for { } lt(q_pc, q_lin_end) { } { + let q_word := mload(q_pc) + let q_ptr := shr(240, q_word) + let qconst := byte(2, q_word) + q_pc := add(q_pc, 3) + q_top := addmod( + q_top, + mulmod(mload(q_ptr), mload(add(q_const_mptr, shl(5, qconst))), r), + r + ) + } + // Product part: q_top += c_i * mload(lhs_i) * mload(rhs_i). + let q_product_end := add(q_pc, mul(q_product_count, 5)) + for { } lt(q_pc, q_product_end) { } { + let q_word := mload(q_pc) + let q_lhs := shr(240, q_word) + let q_rhs := and(shr(224, q_word), 0xffff) + let qconst := byte(4, q_word) + q_pc := add(q_pc, 5) + q_top := addmod( + q_top, + mulmod( + mulmod(mload(q_lhs), mload(q_rhs), r), + mload(add(q_const_mptr, shl(5, qconst))), + r + ), + r + ) + } + } + {%- endif %} + {%- if program.op_usage.lin7 || program.op_usage.bilin7_row || program.op_usage.bilin7_pairwise || program.op_usage.modarith7 %} + // Limb-aware opcodes are opt-in compact forms for + // structurally recognized non-SHA foreign-field shapes. + // Coefficients are indexes into q_const_mptr, which is + // generated from VK/program data, never from proof + // calldata. + // + // Rust source shape: + // proofs/src/plonk/mod.rs::partially_evaluate_identities + // circuits/src/field/foreign/util.rs::{sum_exprs,pair_wise_prod} + // circuits/src/field/foreign/params.rs::{base_powers,double_base_powers} + // + // "Foreign field" means the circuit represents elements + // modulo another modulus m as 7 limbs in base + // 2^LOG2_BASE. The verifier does not switch fields; it + // evaluates the lowered identity over BLS12-381 Fr, using + // Fr coefficients equal to base^i mod m or base^(i+j) mod m. + {%- endif %} + {%- if program.op_usage.lin7 %} + // VM 0x1c LIN7: byte-only 7-term foreign-field linear form. + case {{ template_constants.quotient_vm.op.lin7|hex() }} { + // LIN7: sum_i coeff[i] * value[i] over Fr. + // Typical Rust origin: foreign/gates/norm.rs + // normalization and foreign/gates/mul.rs base-power + // sums for x/y/z limbs. + // + // Operand layout: seven repeated {u8 const_idx, + // u16 ptr} pairs. The result is pushed as a fresh + // stack value. + if q_has_top { + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } + let q_acc := 0 + // Accumulate exactly seven limbs, matching the + // generated foreign-field limb width. + for { let q_i := 0 } lt(q_i, {{ template_constants.quotient_vm.limb_count }}) { q_i := add(q_i, 1) } { + let q_word := mload(q_pc) + let qconst := byte(0, q_word) + let q_ptr := and(shr(232, q_word), 0xffff) + q_pc := add(q_pc, 3) + q_acc := addmod( + q_acc, + mulmod(mload(add(q_const_mptr, shl(5, qconst))), mload(q_ptr), r), + r + ) + } + q_top := q_acc + q_has_top := 1 + } + {%- endif %} + {%- if program.op_usage.bilin7_row %} + // VM 0x1d BILIN7_ROW: byte-only lhs times a 7-term weighted row. + case {{ template_constants.quotient_vm.op.bilin7_row|hex() }} { + // BILIN7_ROW: lhs * sum_i coeff[i] * rhs[i]. + // Typical Rust origin: one row/slice of + // pair_wise_prod in foreign multiplication and EC + // on_curve/slope/tangent/lambda_squared gates. + // + // Operand layout: u16 lhs_ptr, then seven repeated + // {u8 const_idx, u16 rhs_ptr} pairs. The lhs value is + // loaded once and reused for all seven products. + let q_lhs := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + let q_lhs_value := mload(q_lhs) + if q_has_top { + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } + let q_acc := 0 + for { let q_i := 0 } lt(q_i, {{ template_constants.quotient_vm.limb_count }}) { q_i := add(q_i, 1) } { + let q_word := mload(q_pc) + let qconst := byte(0, q_word) + let q_rhs := and(shr(232, q_word), 0xffff) + q_pc := add(q_pc, 3) + q_acc := addmod( + q_acc, + mulmod( + mulmod(q_lhs_value, mload(q_rhs), r), + mload(add(q_const_mptr, shl(5, qconst))), + r + ), + r + ) + } + q_top := q_acc + q_has_top := 1 + } + {%- endif %} + {%- if program.op_usage.bilin7_pairwise %} + // VM 0x1e BILIN7_PAIRWISE: byte-only 7-by-7 weighted convolution. + case {{ template_constants.quotient_vm.op.bilin7_pairwise|hex() }} { + // BILIN7_PAIRWISE: + // sum_{i=0..6,j=0..6} coeff[i+j] * lhs[i] * rhs[j]. + // Bases point to contiguous 7-word limb vectors. + // Typical Rust origin: + // sum_exprs(double_base_powers, + // pair_wise_prod(lhs, rhs)) + // where double_base_powers[k] = base^k mod m. + // + // Operand layout: u16 lhs_base, u16 rhs_base, then 13 + // one-byte coefficient indexes. Coefficients are + // addressed by i+j, so 7-by-7 products need 13 slots. + let q_word := mload(q_pc) + let q_lhs_base := shr(240, q_word) + let q_rhs_base := and(shr(224, q_word), 0xffff) + q_pc := add(q_pc, {{ template_constants.quotient_vm.byte_u32_bytes|hex() }}) + let q_coeff_pc := q_pc + q_pc := add(q_pc, {{ template_constants.quotient_vm.limb_pairwise_coeffs }}) + if q_has_top { + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } + let q_acc := 0 + // Nested limb convolution over two contiguous + // 7-word vectors. + for { let q_i := 0 } lt(q_i, {{ template_constants.quotient_vm.limb_count }}) { q_i := add(q_i, 1) } { + let q_lhs_value := mload(add(q_lhs_base, shl(5, q_i))) + for { let q_j := 0 } lt(q_j, {{ template_constants.quotient_vm.limb_count }}) { q_j := add(q_j, 1) } { + let qconst := byte(0, mload(add(q_coeff_pc, add(q_i, q_j)))) + q_acc := addmod( + q_acc, + mulmod( + mulmod(q_lhs_value, mload(add(q_rhs_base, shl(5, q_j))), r), + mload(add(q_const_mptr, shl(5, qconst))), + r + ), + r + ) + } + } + q_top := q_acc + q_has_top := 1 + } + {%- endif %} + {%- if program.op_usage.modarith7 %} + // VM 0x21 MODARITH7: byte-only fused affine 7-limb foreign-field/ECC identity. + case {{ template_constants.quotient_vm.op.modarith7|hex() }} { + // MODARITH7: + // maybe_cond * ( + // c + // + sum LIN7 blocks + // + sum BILIN7_ROW blocks + // + sum BILIN7_PAIRWISE blocks + // + sum coeff[k] * mload(ptr[k]) + // + sum coeff[k] * mload(lhs[k]) * mload(rhs[k]) + // ) + // It is a dispatch/operand-load optimization only; + // all coefficients still come from the generated + // quotient constant table. + // + // Flags: + // bit 0: multiply the final affine sum by a memory + // condition word. + // bit 1: seed q_acc from a constant-table word + // before reading the counted term blocks. + let q_flags := byte(0, mload(q_pc)) + q_pc := add(q_pc, 1) + let q_cond_ptr := 0 + if and(q_flags, 0x01) { + // Optional condition pointer. When present, the + // whole identity is gated by mload(q_cond_ptr). + q_cond_ptr := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + } + + let q_acc := 0 + if and(q_flags, 0x02) { + // Optional constant seed for affine identities + // with a standalone constant term. + let qconst := byte(0, mload(q_pc)) + q_pc := add(q_pc, 1) + q_acc := mload(add(q_const_mptr, shl(5, qconst))) + } + + // Five one-byte counters describe the blocks that + // follow. Each block has a fixed-width internal layout, + // so q_pc can advance without per-term tags. + let q_counts_word := mload(q_pc) + let q_lin_count := byte(0, q_counts_word) + let q_row_count := byte(1, q_counts_word) + let q_pairwise_count := byte(2, q_counts_word) + let q_mem_count := byte(3, q_counts_word) + let q_product_count := byte(4, q_counts_word) + q_pc := add(q_pc, 5) + + if q_has_top { + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } + + // LIN7 blocks: q_acc += sum_i c_i * limb_i. + for { let q_lin_block := 0 } lt(q_lin_block, q_lin_count) { q_lin_block := add(q_lin_block, 1) } { + for { let q_i := 0 } lt(q_i, {{ template_constants.quotient_vm.limb_count }}) { q_i := add(q_i, 1) } { + let q_word := mload(q_pc) + let qconst := byte(0, q_word) + let q_ptr := and(shr(232, q_word), 0xffff) + q_pc := add(q_pc, 3) + q_acc := addmod( + q_acc, + mulmod(mload(add(q_const_mptr, shl(5, qconst))), mload(q_ptr), r), + r + ) + } + } + + // BILIN7_ROW blocks: q_acc += lhs * sum_i c_i * rhs_i. + for { let q_row_block := 0 } lt(q_row_block, q_row_count) { q_row_block := add(q_row_block, 1) } { + let q_lhs := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + let q_lhs_value := mload(q_lhs) + for { let q_i := 0 } lt(q_i, {{ template_constants.quotient_vm.limb_count }}) { q_i := add(q_i, 1) } { + let q_word := mload(q_pc) + let qconst := byte(0, q_word) + let q_rhs := and(shr(232, q_word), 0xffff) + q_pc := add(q_pc, 3) + q_acc := addmod( + q_acc, + mulmod( + mulmod(q_lhs_value, mload(q_rhs), r), + mload(add(q_const_mptr, shl(5, qconst))), + r + ), + r + ) + } + } + + // BILIN7_PAIRWISE blocks: q_acc += weighted 7-by-7 + // product convolution. + for { let q_pair_block := 0 } lt(q_pair_block, q_pairwise_count) { q_pair_block := add(q_pair_block, 1) } { + let q_pair_word := mload(q_pc) + let q_lhs_base := shr(240, q_pair_word) + let q_rhs_base := and(shr(224, q_pair_word), 0xffff) + q_pc := add(q_pc, {{ template_constants.quotient_vm.byte_u32_bytes|hex() }}) + let q_coeff_pc := q_pc + q_pc := add(q_pc, {{ template_constants.quotient_vm.limb_pairwise_coeffs }}) + for { let q_i := 0 } lt(q_i, {{ template_constants.quotient_vm.limb_count }}) { q_i := add(q_i, 1) } { + let q_lhs_value := mload(add(q_lhs_base, shl(5, q_i))) + for { let q_j := 0 } lt(q_j, {{ template_constants.quotient_vm.limb_count }}) { q_j := add(q_j, 1) } { + let qconst := byte(0, mload(add(q_coeff_pc, add(q_i, q_j)))) + q_acc := addmod( + q_acc, + mulmod( + mulmod(q_lhs_value, mload(add(q_rhs_base, shl(5, q_j))), r), + mload(add(q_const_mptr, shl(5, qconst))), + r + ), + r + ) + } + } + } + + // Extra linear memory terms outside the 7-limb shapes. + for { let q_mem_block := 0 } lt(q_mem_block, q_mem_count) { q_mem_block := add(q_mem_block, 1) } { + let q_word := mload(q_pc) + let qconst := byte(0, q_word) + let q_ptr := and(shr(232, q_word), 0xffff) + q_pc := add(q_pc, 3) + q_acc := addmod( + q_acc, + mulmod(mload(add(q_const_mptr, shl(5, qconst))), mload(q_ptr), r), + r + ) + } + + // Extra binary product terms outside the 7-limb shapes. + for { let q_product_block := 0 } lt(q_product_block, q_product_count) { q_product_block := add(q_product_block, 1) } { + let q_word := mload(q_pc) + let qconst := byte(0, q_word) + let q_lhs := and(shr(232, q_word), 0xffff) + let q_rhs := and(shr(216, q_word), 0xffff) + q_pc := add(q_pc, 5) + q_acc := addmod( + q_acc, + mulmod( + mulmod(mload(q_lhs), mload(q_rhs), r), + mload(add(q_const_mptr, shl(5, qconst))), + r + ), + r + ) + } + + if and(q_flags, 0x01) { + // Apply the optional gate condition last so every + // subterm shares the same selector/condition. + q_acc := mulmod(mload(q_cond_ptr), q_acc, r) + } + // MODARITH7 pushes its fused identity value. + q_top := q_acc + q_has_top := 1 + } + {%- endif %} + {%- if program.op_usage.native_permutation && quotient_native_permutation_computation.len() > 0 %} + // Native permutation callback. It evaluates the + // permutation identities from permutation.rs at this exact + // VM position, preserving the Rust identity order while + // avoiding a large interpreted product loop. + // VM 0x19 NATIVE_PERMUTATION: marker for the generated permutation callback. + case {{ template_constants.quotient_vm.op.native_permutation|hex() }} { + // Native callbacks are identity-boundary opcodes. They + // must not inherit any partially evaluated VM stack + // state from the previous expression. + q_top := 0 + q_has_top := 0 + // The generated loop below uses program.stack_mptr as + // its scratch-table base, not as a conventional VM + // stack. The Rust memory planner must reserve enough + // words for structured_permutation_scratch_words(meta) + // whenever this opcode can appear. + q_sp := {{ program.stack_mptr|hex() }} + // The generated lines below call the same fold snippets + // used by interpreted expressions, so trace IDs and + // y-batch positions remain contiguous. + {%- for line in quotient_native_permutation_computation %} + {{ line }} + {%- endfor %} + } + {%- endif %} + {%- if program.op_usage.native_lookup && quotient_native_lookup_computation.len() > 0 %} + // Native lookup callback. This whole-family opcode + // evaluates the LogUp boundary, helper-chunk, and + // accumulator identities at this VM position, preserving + // the Rust y-batch order while avoiding many interpreted + // product-loop opcodes. + // VM 0x1f NATIVE_LOOKUP: marker for the generated LogUp lookup callback. + case {{ template_constants.quotient_vm.op.native_lookup|hex() }} { + // Reset VM stack state before entering structured + // lookup Yul. Lookup callbacks own their scratch + // layout and perform all needed folds internally. + q_top := 0 + q_has_top := 0 + // The generated loop below uses program.stack_mptr as + // f+beta/prefix/suffix scratch rather than as a + // conventional VM stack. The Rust memory planner must + // reserve structured_lookup_scratch_words(meta). + q_sp := {{ program.stack_mptr|hex() }} + // Generated LogUp code follows the same y-batch order + // as the Rust identity stream. + {%- for line in quotient_native_lookup_computation %} + {{ line }} + {%- endfor %} + } + {%- endif %} + {%- if program.op_usage.native_identity && quotient_native_identity_computations.len() > 0 %} + // Native callbacks are generated only for the heaviest + // recognized Midfall gate identities. All other gate and + // non-native identity arithmetic remains in + // the compact q_program VM above, preserving the Rust + // `partially_evaluate_identities` order. + // VM 0x1b NATIVE_IDENTITY: marker for generated heavy-gate callbacks. + case {{ template_constants.quotient_vm.op.native_identity|hex() }} { + // Operand layout: u16 native callback index. The + // manifest validates that callback indexes appear in + // generated order and target existing switch cases. + let q_native_idx := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + // Heavy identities are whole expressions, so clear the + // interpreter stack before dispatching. + q_top := 0 + q_has_top := 0 + q_sp := {{ program.stack_mptr|hex() }} + // Native identity sub-cases are generated from selected heavy gate identities. + switch q_native_idx + {%- for code_block in quotient_native_identity_computations %} + case {{ loop.index0 }} { + {%- for line in code_block %} + {{ line }} + {%- endfor %} + } + {%- endfor %} + default { revert(0, 0) } + } + {%- endif %} + {%- if program.op_usage.fold_main %} + // VM 0x0a FOLD_MAIN: consume q_top into the fully evaluated numerator fold. + case {{ template_constants.quotient_vm.op.fold_main|hex() }} { + // q_top is the complete value of one fully evaluated + // identity at x. It leaves the expression stack here. + let q_eval := q_top + q_has_top := 0 + {%- if self.trace %} + trace_u256(mload({{ program.trace_id_mptr|hex() }}), q_eval) + mstore({{ program.trace_id_mptr|hex() }}, add(mload({{ program.trace_id_mptr|hex() }}), 1)) + {%- endif %} + // Fully-evaluated identity: qn = qn*y + eval. + // This matches the reverse y-power fold in Rust + // linearization once all identities have been read. + mstore({{ program.eval_numer_mptr|hex() }}, mulmod(mload({{ program.eval_numer_mptr|hex() }}), y, r)) + mstore({{ program.eval_numer_mptr|hex() }}, addmod(mload({{ program.eval_numer_mptr|hex() }}), q_eval, r)) + } + {%- endif %} + {%- if program.op_usage.fold_selector %} + // VM 0x0b FOLD_SELECTOR: consume q_top into one simple-selector bucket. + case {{ template_constants.quotient_vm.op.fold_selector|hex() }} { + // Operand layout packed into three bytes: + // high byte: selector bucket index; + // low u16 : y-power gap since this selector's + // previous contribution. + let q_selector_payload := shr(232, mload(q_pc)) + q_pc := add(q_pc, 3) + let q_sel_idx := shr(16, q_selector_payload) + let q_sel_gap := and(q_selector_payload, 0xffff) + let q_eval := q_top + q_has_top := 0 + {%- if self.trace %} + trace_u256(mload({{ program.trace_id_mptr|hex() }}), q_eval) + mstore({{ program.trace_id_mptr|hex() }}, add(mload({{ program.trace_id_mptr|hex() }}), 1)) + {%- endif %} + // Simple-selector identity: keep the same y-batch + // position as main identities, then advance only this + // selector bucket by its codegen-known gap. + // + // The global fully-evaluated accumulator is still + // multiplied by y so later main identities land at the + // same y powers as Rust's reverse fold. + mstore({{ program.eval_numer_mptr|hex() }}, mulmod(mload({{ program.eval_numer_mptr|hex() }}), y, r)) + let q_target_ptr := add(SELECTOR_ACC_MPTR, shl(5, q_sel_idx)) + let q_sel_acc := mload(q_target_ptr) + if q_sel_gap { + // Selector buckets are sparse in the global + // identity stream. Precomputed y^gap advances only + // this selector's local accumulator. + q_sel_acc := mulmod(q_sel_acc, mload(add({{ program.selector_power_mptr|hex() }}, shl(5, q_sel_gap))), r) + } + mstore(q_target_ptr, addmod(q_sel_acc, q_eval, r)) + } + {%- endif %} + // Invalid generated bytecode should fail closed. 0x1a intentionally lands here. + default { + revert(0, 0) + } + } + + // Structured post-VM suffix. The current default uses this for + // regular trash constraints: it is smaller than fully unrolled + // Yul and cheaper than interpreting every trash operation. + // + // These generated blocks run after q_pc reaches q_end, but + // they still participate in the same identity order and write + // into the same numerator / selector accumulators. + {%- for code_block in quotient_post_vm_computations %} + {%- for line in code_block %} + {{ line }} + {%- endfor %} + {%- endfor %} + + {%- if simple_selector_cols.len() > 0 %} + // Finish selector buckets by applying the codegen-known tail + // from each selector's last identity to the end of the global + // y-batch. + // + // After this step, every selector bucket is aligned with the + // final global y position and can be multiplied by its fixed + // selector commitment in the linearized MSM. + {%- for tail in program.selector_tail_updates %} + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, {{ tail.selector_offset|hex() }}) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add({{ program.selector_power_mptr|hex() }}, {{ tail.power_offset|hex() }})), r)) + } + {%- endfor %} + {%- endif %} + + // Fully evaluated identities are the constant-polynomial side + // of the linearization query. Rust subtracts that grouped + // scalar into expected_eval, so Solidity stores -nu_y(x). + let linearization_expected_eval := addmod(0, sub(r, mload({{ program.eval_numer_mptr|hex() }})), r) + mstore(QUOTIENT_EVAL_MPTR, linearization_expected_eval) + pop(y) + {%- when None %} + // Legacy/direct mode. This path emits the numerator + // reconstruction directly instead of interpreting q_program. + // It is used by older monolithic/experimental generation + // modes and remains available as a fallback for models that do + // not carry compact quotient bytecode. + // + // The generated statements in quotient_eval_numer_computations + // are already ordered and folded by the Rust renderer. They + // must leave `quotient_eval_numer` as nu_y(x), matching the + // compact VM accumulator in program.eval_numer_mptr. + let delta := {{ fr_delta }} // BLS12-381 Fr::DELTA + let y := mload(Y_MPTR) + {%- if self.trace %} + // Direct mode increments q_trace_id inside generated fold + // snippets, mirroring compact-mode trace_id_mptr. + let q_trace_id := {{ quotient_identity_trace_base }} + {%- endif %} + + // Direct/generated numerator body. This may contain unrolled + // gate, permutation, lookup, trash, and selector-fold code. + {%- for code_block in quotient_eval_numer_computations %} + {%- for line in code_block %} + {{ line }} + {%- endfor %} + {%- endfor %} + + // Silence Yul unused-variable warnings in renders where the + // direct body did not need one of these common constants. + pop(y) + pop(delta) + + // Store the expected opening scalar for the linearized + // commitment at x: the negated sum of fully-evaluated + // identities. See + // `compute_linearization_commitment` in + // midfall/proofs/src/plonk/linearization/verifier.rs: + // + // expected_eval -= eval (for col_idx == None) + // + // The commitment side already includes the quotient-limb + // factor (1 - x^n), so this scalar is -nu_y(x), not + // h(x) = nu_y(x) / (x^n - 1). + let linearization_expected_eval := addmod(0, sub(r, quotient_eval_numer), r) + mstore(QUOTIENT_EVAL_MPTR, linearization_expected_eval) + {%- endmatch %} + } diff --git a/proofs/solidity-verifier/templates/partials/verifier/AccumulatorHelpers.yul b/proofs/solidity-verifier/templates/partials/verifier/AccumulatorHelpers.yul new file mode 100644 index 000000000..d872afb9d --- /dev/null +++ b/proofs/solidity-verifier/templates/partials/verifier/AccumulatorHelpers.yul @@ -0,0 +1,396 @@ + // ---------- IVC accumulator public-input decoding ---------- + // + // `AssignedForeignPoint` exposes each base-field coordinate + // through `AssignedField::as_public_input`: seven radix-2^56 limbs of + // (coord - 1) are packed four-at-a-time into native field elements. + // The x coordinate's first packed word carries the identity flag by + // adding one raw radix base. Rebuild EIP-2537 padded + // (x_hi, x_lo, y_hi, y_lo) words from that encoding. + // + // Public-input layout for one coordinate: + // word 0: limb_0 | limb_1 << bits | ... up to limbs_per_word + // word 1: next limbs, if any + // + // The limbs are little-endian in the represented integer even + // though calldata words are loaded as big 256-bit values. The loop + // below extracts each limb by shifting inside the packed word and + // reconstructs the full coordinate into the two-word EIP-2537 + // representation expected by the BLS12-381 precompiles. + function load_acc_coord_shifted(src, bits, n, base, limbs_per_word, first_adjust) -> hi, lo { + // Mask for one radix limb, e.g. 2^56 - 1 for the current + // BLS12-381 self-emulation parameters. + let mask := sub(base, 1) + for { let i := 0 } lt(i, n) { i := add(i, 1) } { + // Limb words are little-endian packed inside each Fr + // public input. `first_adjust` removes the identity flag + // base from the first x word when present. + let packed := calldataload(add(src, mul(div(i, limbs_per_word), 0x20))) + if and(iszero(div(i, limbs_per_word)), first_adjust) { + packed := sub(packed, first_adjust) + } + // Select limb i from its packed field word. The mod/div + // pair maps a limb index to an intra-word limb slot and + // the calldata word containing it. + let limb := and(shr(mul(mod(i, limbs_per_word), bits), packed), mask) + + let shift := mul(i, bits) + // Split the reconstructed 384-bit coordinate into the + // EIP-2537 high/low words expected by the precompiles. + if lt(shift, 256) { + lo := add(lo, shl(shift, limb)) + if gt(add(shift, bits), 256) { + // A limb can straddle the 256-bit low/high split. + // Move the overflow bits into hi. + hi := add(hi, shr(sub(256, shift), limb)) + } + } + if iszero(lt(shift, 256)) { + // Once shift >= 256 the whole limb belongs to hi. + hi := add(hi, shl(sub(shift, 256), limb)) + } + } + } + + // The shifted coordinate codec represents zero as p-1 before the + // final +1 below, so keep this sentinel explicit. + function is_bls_p_minus_one(hi, lo) -> yes { + yes := and(eq(hi, BLS_P_HI), eq(lo, BLS_P_MINUS_ONE_LO)) + } + + // Canonical encoded accumulator identity: + // x = p-1 plus the identity flag in the first packed word, + // y = p-1 with no identity flag. + // It decodes to the EIP-2537 point-at-infinity slot (all zeros). + // + // This fast path is deliberately stricter than "decodes to zero": + // the point at infinity has exactly one accepted public-input + // encoding. Non-canonical zero-like encodings are rejected later. + function is_acc_encoded_identity(src) -> yes { + yes := and( + and( + eq(calldataload(src), BLS_P_MINUS_ONE_PACKED_0_WITH_ID_FLAG), + eq(calldataload(add(src, 0x20)), BLS_P_MINUS_ONE_PACKED_1) + ), + and( + eq(calldataload(add(src, 0x40)), BLS_P_MINUS_ONE_PACKED_0), + eq(calldataload(add(src, 0x60)), BLS_P_MINUS_ONE_PACKED_1) + ) + ) + } + + // Reject unused high bits in the packed public-input words. This + // makes each accumulator point encoding canonical before it reaches + // the precompile-based curve/subgroup validation. + function check_acc_coord_packing(src, bits, n, limbs_per_word) -> ok { + ok := 1 + // Number of packed native-field public-input words occupied by + // one coordinate. + let coord_words := div(add(n, sub(limbs_per_word, 1)), limbs_per_word) + for { let word_idx := 0 } lt(word_idx, coord_words) { word_idx := add(word_idx, 1) } { + // The final word may contain fewer than limbs_per_word + // limbs. Any unused high bits must be zero, otherwise the + // same coordinate would have multiple calldata encodings. + let remaining := sub(n, mul(word_idx, limbs_per_word)) + let limbs_in_word := limbs_per_word + if lt(remaining, limbs_per_word) { + limbs_in_word := remaining + } + let used_bits := mul(limbs_in_word, bits) + if lt(used_bits, 256) { + // shl(used_bits, 1) == 2^used_bits. The packed word + // must be strictly less than that bound. + ok := and(ok, lt(calldataload(add(src, mul(word_idx, 0x20))), shl(used_bits, 1))) + } + } + } + + // Decode one shifted coordinate. `allow_id` is true only for x, + // because the identity flag lives in x's first packed word. + function load_acc_coord(src, allow_id, bits, n, base, limbs_per_word) -> ok, hi, lo, is_id { + ok := check_acc_coord_packing(src, bits, n, limbs_per_word) + if and(allow_id, iszero(lt(calldataload(src), base))) { + // Probe the x identity flag by removing one radix base and + // checking whether the adjusted coordinate is p-1. + // + // `calldataload(src) >= base` is a cheap prefilter: only x + // can carry this flag, and adding one radix base must make + // the first packed word at least base. + let adj_hi, adj_lo := load_acc_coord_shifted(src, bits, n, base, limbs_per_word, base) + is_id := is_bls_p_minus_one(adj_hi, adj_lo) + } + + // Decode again with the identity adjustment applied only when + // the canonical identity flag was actually detected. + hi, lo := load_acc_coord_shifted(src, bits, n, base, limbs_per_word, mul(is_id, base)) + ok := and( + ok, + // Coordinate must be in the BLS12-381 base field, i.e. + // <= p - 1 in split hi/lo form. + or(lt(hi, BLS_P_HI), and(eq(hi, BLS_P_HI), iszero(gt(lo, BLS_P_MINUS_ONE_LO)))) + ) + + let was_p_minus_one := is_bls_p_minus_one(hi, lo) + if was_p_minus_one { + // Shifted encoding maps p-1 back to zero. + hi := 0 + lo := 0 + } + if iszero(was_p_minus_one) { + // All other coordinates are encoded as coord - 1, so add + // one back with carry into the high word. + let next_lo := add(lo, 1) + hi := add(hi, lt(next_lo, lo)) + lo := next_lo + } + + // EIP-2537 pads each 48-byte Fp coordinate to 64 bytes, + // so the high word must fit in its low 128 bits. + // This also catches impossible reconstructions above 384 bits. + ok := and(ok, lt(hi, shl(128, 1))) + } + + // Decode a public accumulator point into an EIP-2537 4-word G1 + // slot. Non-identity points are curve/subgroup checked later by + // routing them through G1MSM. + function load_acc_point(dst, src, bits, n, base) -> ok, is_id { + // Prefer the canonical all-coordinate identity encoding before + // attempting coordinate-level shifted decoding. This accepts + // the point at infinity only in the exact form generated by the + // circuit's public-input codec. + is_id := is_acc_encoded_identity(src) + if is_id { + ok := 1 + // EIP-2537 encodes G1 identity as four zero words: + // x_hi = x_lo = y_hi = y_lo = 0. + mstore(dst, 0) + mstore(add(dst, 0x20), 0) + mstore(add(dst, 0x40), 0) + mstore(add(dst, 0x60), 0) + } + if iszero(is_id) { + // x occupies coord_words packed public-input words; y + // starts immediately after x. + let limbs_per_word := {{ template_constants.accumulator.limbs_per_word }} + let coord_words := div(add(n, sub(limbs_per_word, 1)), limbs_per_word) + // Only x may carry the identity flag. y must decode as a + // normal shifted coordinate. + let x_ok, x_hi, x_lo, x_is_id := load_acc_coord(src, 1, bits, n, base, limbs_per_word) + let y_ok, y_hi, y_lo, y_id := load_acc_coord( + add(src, mul(coord_words, 0x20)), + 0, + bits, + n, + base, + limbs_per_word + ) + // y_id is always zero because allow_id was false, but the + // tuple shape is shared with x decoding. + pop(y_id) + ok := and(x_ok, y_ok) + is_id := x_is_id + + if is_id { + // If x carried the identity flag, both decoded + // coordinates must be zero after shifting. Any other y + // value would be a malformed infinity encoding. + ok := and(ok, iszero(or(or(x_hi, x_lo), or(y_hi, y_lo)))) + mstore(dst, 0) + mstore(add(dst, 0x20), 0) + mstore(add(dst, 0x40), 0) + mstore(add(dst, 0x60), 0) + } + if iszero(is_id) { + // The coordinate codec maps encoded p-1 to decoded + // zero. EIP-2537 reserves affine (0,0) for the point + // at infinity, so a decoded infinity is only valid + // when the canonical accumulator identity encoding + // was used above. + let decoded_zero := iszero(or(or(x_hi, x_lo), or(y_hi, y_lo))) + ok := and(ok, iszero(decoded_zero)) + // Store the affine point in the exact precompile input + // layout: x_hi, x_lo, y_hi, y_lo. + mstore(dst, x_hi) + mstore(add(dst, 0x20), x_lo) + mstore(add(dst, 0x40), y_hi) + mstore(add(dst, 0x60), y_lo) + } + } + } + + {%- if self.expected_has_accumulator %} + // Validate and prepare the public accumulator equation before the + // main transcript starts. This fails malformed public inputs early + // and writes ACC_LHS_MPTR / ACC_RHS_MPTR for final pairing batching. + // + // The accumulator public input represents an equality of two G1 + // commitments used by the recursive KZG accumulator. This helper: + // 1. decodes carried public G1 points from shifted limbs; + // 2. forces every decoded point through EIP-2537 G1MSM so the + // precompile validates curve/subgroup membership; + // 3. folds the RHS carried point and fixed-base scalar tail into + // ACC_RHS_MPTR, leaving ACC_LHS_MPTR / ACC_RHS_MPTR ready for + // randomized batching in FinalPairing.yul. + function validate_public_accumulator(success, r) -> out { + out := success + let bits := {{ self.expected_num_acc_limb_bits }} + let n := {{ self.expected_num_acc_limbs }} + // The BLS12-381 self-emulation currently exposes Fp + // coordinates as 7 radix-2^56 limbs. + let limb_base := shl(bits, 1) + let limbs_per_word := {{ template_constants.accumulator.limbs_per_word }} + let coord_words := div(add(n, sub(limbs_per_word, 1)), limbs_per_word) + // acc_offset is generated from the VK/protocol shape and + // points into the ABI `instances` array. + let acc_instance_ptr := add(INSTANCE_CPTR, {{ (self.expected_acc_offset * 32)|hex() }}) + + // LHS layout: point limbs (x,y), then either an explicit + // scalar word or an implicit unit scalar for already-collapsed + // point-pair public inputs. + // The scalar pointer is computed unconditionally; the rendered + // branch below decides whether to read it or use scalar 1. + let lhs_scalar_ptr := add(acc_instance_ptr, mul(mul(2, coord_words), 0x20)) + let lhs_ok, lhs_is_id := load_acc_point(ACC_LHS_MPTR, acc_instance_ptr, bits, n, limb_base) + out := and(out, lhs_ok) + // Shared scratch for one-pair LHS validation and the later + // variable-length RHS MSM. + let acc_scratch := {{ memory.acc_msm_scratch|hex() }} + { + {%- if self.expected_acc_has_carried_scalars %} + // Carried-scalar layout: the circuit exposes the scalar + // that multiplies the carried LHS point. + let lhs_scalar := calldataload(lhs_scalar_ptr) + {%- else %} + // Already-collapsed point-pair layout: carried scalars are + // implicit one. + let lhs_scalar := 1 + {%- endif %} + // Identity status is useful for decoding checks above, but + // validation still goes through G1MSM for all points. + pop(lhs_is_id) + // Always route the decoded carried point through G1MSM, + // even for identity points and zero/one scalars. The + // precompile is the on-curve/subgroup validator for this + // public-input point; skipping it would let a malformed + // non-identity point hide behind scalar 0. + mcopy(acc_scratch, ACC_LHS_MPTR, 0x80) + mstore(add(acc_scratch, 0x80), lhs_scalar) + if out { + // Single-pair MSM output overwrites ACC_LHS_MPTR with + // lhs_scalar * decoded_lhs. If lhs_scalar is one, this + // is also a curve/subgroup validation round-trip. + out := staticcall({{ g1msm_single_gas_cap }}, {{ template_constants.eip2537.g1msm_address|hex() }}, acc_scratch, {{ template_constants.g1_msm_pair_bytes|hex() }}, ACC_LHS_MPTR, {{ template_constants.g1_bytes|hex() }}) + out := and(out, eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) + } + } + + {%- if acc_fixed_bases.len() == 0 %} + {%- if self.expected_acc_has_carried_scalars %} + // RHS layout for this generated verifier is fully collapsed: + // point limbs (x,y), scalar. There is no fixed-base scalar + // tail; fixed-base contributions were already folded into + // ACC_RHS by the circuit/native accumulator construction. + {%- else %} + // RHS layout for this generated verifier is an already + // collapsed point pair: lhs point, rhs point. Both carried + // scalars are implicit one, and there is no fixed-base scalar + // tail. + {%- endif %} + {%- else %} + // RHS layout for this generated verifier is partially + // collapsed: point limbs (x,y), scalar, then fixed-base + // scalars in BTreeMap key order (`-G`, fixed_i, perm_i + // lexicographically by name). Each tail scalar is consumed + // below and appended to the RHS MSM with its generated base. + {%- endif %} + {%- if self.expected_acc_has_carried_scalars %} + let rhs_instance_ptr := add(lhs_scalar_ptr, 0x20) + {%- else %} + let rhs_instance_ptr := lhs_scalar_ptr + {%- endif %} + // RHS scalar, when present, immediately follows the RHS point + // limbs. The fixed-base scalar tail starts after it. + let rhs_scalar_ptr := add(rhs_instance_ptr, mul(mul(2, coord_words), 0x20)) + let rhs_ok, rhs_is_id := load_acc_point(ACC_RHS_MPTR, rhs_instance_ptr, bits, n, limb_base) + out := and(out, rhs_ok) + // acc_pair_ptr appends (G1, scalar) pairs into acc_scratch for + // one final RHS MSM. + let acc_pair_ptr := acc_scratch + { + {%- if self.expected_acc_has_carried_scalars %} + // Explicit carried RHS scalar. + let rhs_scalar := calldataload(rhs_scalar_ptr) + {%- else %} + // Implicit unit scalar for already-collapsed point pairs. + let rhs_scalar := 1 + {%- endif %} + pop(rhs_is_id) + // Keep the carried RHS point in the MSM input even when + // it is encoded as identity or has scalar 0/1, so EIP-2537 + // validates every decoded public accumulator point before + // it can affect, or be erased from, the pairing batch. + mcopy(acc_pair_ptr, ACC_RHS_MPTR, {{ template_constants.g1_bytes|hex() }}) + mstore(add(acc_pair_ptr, {{ template_constants.g1_bytes|hex() }}), rhs_scalar) + // Move to the next (G1, scalar) pair slot. + acc_pair_ptr := add(acc_pair_ptr, {{ template_constants.g1_msm_pair_bytes|hex() }}) + } + {%- if acc_fixed_bases.len() > 0 %} + {%- if self.expected_acc_has_carried_scalars %} + // Fixed-base tail starts after the explicit RHS scalar. + let fixed_scalar_ptr := add(rhs_scalar_ptr, 0x20) + {%- else %} + // Without carried scalars, the tail starts where an explicit + // RHS scalar would otherwise have lived. + let fixed_scalar_ptr := rhs_scalar_ptr + {%- endif %} + {%- for (base_mptr, negate_scalar) in acc_fixed_bases %} + // Generated fixed-base scalar {{ loop.index0 }}. The + // corresponding base point is embedded in verifier memory at + // {{ base_mptr|hex() }}. + let fixed_scalar_{{ loop.index0 }} := calldataload(fixed_scalar_ptr) + {%- if negate_scalar %} + // Some accumulator bases are represented with a negated scalar + // so the MSM can reuse the generated positive base point. + fixed_scalar_{{ loop.index0 }} := mod(sub(r, fixed_scalar_{{ loop.index0 }}), r) + {%- endif %} + if fixed_scalar_{{ loop.index0 }} { + // Zero scalars do not need a pair in the RHS MSM. Nonzero + // tail scalars append their generated fixed base. + mcopy(acc_pair_ptr, {{ base_mptr|hex() }}, {{ template_constants.g1_bytes|hex() }}) + mstore(add(acc_pair_ptr, {{ template_constants.g1_bytes|hex() }}), fixed_scalar_{{ loop.index0 }}) + acc_pair_ptr := add(acc_pair_ptr, {{ template_constants.g1_msm_pair_bytes|hex() }}) + } + // Advance to the next public tail scalar regardless of whether + // this one was zero and omitted from the MSM input. + fixed_scalar_ptr := add(fixed_scalar_ptr, 0x20) + {%- endfor %} + {%- endif %} + // Total byte length of the appended RHS MSM input pairs. This + // is at least one pair because the carried RHS point is always + // appended; keep the guard for synthetic render configurations. + let acc_msm_len := sub(acc_pair_ptr, acc_scratch) + if acc_msm_len { + // Fold the carried RHS point and any generated fixed-base + // tail into ACC_RHS_MPTR. The later final pairing block + // randomizes this equation together with the KZG pairing. + if out { + // Output overwrites ACC_RHS_MPTR with: + // rhs_scalar * carried_rhs + // + sum_i fixed_scalar_i * fixed_base_i + // + // The precompile also validates every nonzero fixed + // base embedded by codegen and the carried RHS point. + out := staticcall( + {{ acc_rhs_g1msm_gas_cap }}, + {{ template_constants.eip2537.g1msm_address|hex() }}, + acc_scratch, + acc_msm_len, + ACC_RHS_MPTR, + {{ template_constants.g1_bytes|hex() }} + ) + out := and(out, eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) + } + } + // The caller checks `out` and reverts before transcript work if + // any decode, canonicality, or precompile validation failed. + } + {%- endif %} diff --git a/proofs/solidity-verifier/templates/partials/verifier/AssemblyHelpers.yul b/proofs/solidity-verifier/templates/partials/verifier/AssemblyHelpers.yul new file mode 100644 index 000000000..4b704dd3c --- /dev/null +++ b/proofs/solidity-verifier/templates/partials/verifier/AssemblyHelpers.yul @@ -0,0 +1,232 @@ + // Inverse of a Fr scalar via modexp(x, r-2, r). The verifier + // calls this only after transcript absorption is complete, so it + // reuses the dead transcript buffer just below VK_MPTR instead of + // a fixed post-VK address that can collide with live PCS scratch + // when the VK payload becomes smaller. + function scalar_inv(x) -> inv { + // Zero has no multiplicative inverse in Fr; callers rely on a + // revert here rather than a bogus modexp result. + if iszero(x) { revert(0, 0) } + let p := {{ memory.scalar_inv_scratch_mptr|hex() }} + // EIP-198 modexp frame: + // [base_len, exp_len, mod_len, base, exponent, modulus] + mstore(add(p, {{ template_constants.modexp.base_len_offset|hex() }}), {{ template_constants.word_bytes|hex() }}) // base len + mstore(add(p, {{ template_constants.modexp.exp_len_offset|hex() }}), {{ template_constants.word_bytes|hex() }}) // exp len + mstore(add(p, {{ template_constants.modexp.mod_len_offset|hex() }}), {{ template_constants.word_bytes|hex() }}) // mod len + mstore(add(p, {{ template_constants.modexp.base_offset|hex() }}), x) + mstore(add(p, {{ template_constants.modexp.exp_offset|hex() }}), sub(FR_MODULUS, 2)) + mstore(add(p, {{ template_constants.modexp.mod_offset|hex() }}), FR_MODULUS) + if iszero(staticcall(gas(), {{ template_constants.modexp.address|hex() }}, p, {{ template_constants.modexp.frame_bytes|hex() }}, p, {{ template_constants.modexp.output_bytes|hex() }})) { revert(0, 0) } + if iszero(eq(returndatasize(), {{ template_constants.modexp.output_bytes|hex() }})) { revert(0, 0) } + inv := mload(p) + } + + // ---------- Streaming Keccak256 transcript helpers ---------- + // + // The transcript buffer lives at + // memory[TRANSCRIPT_MPTR..buf_len). On verifier entry it starts + // empty. Each common(input) appends raw bytes. squeeze_*(buf_len) + // computes one Keccak digest, reseeds the buffer with that + // 32-byte digest, and samples a Fq element as + // uint256(digest_be) mod r. + + function transcript_init() -> buf_len { + // Empty transcript buffer starts exactly at TRANSCRIPT_MPTR. + buf_len := TRANSCRIPT_MPTR + } + + // Append one 32-byte big-endian field/transcript word at the + // current end of the transcript buffer. + function common_word(buf_len, word) -> ret { + mstore(buf_len, word) + ret := add(buf_len, 32) + } + + // Absorb a BLS12-381 G1 point in EIP-2537 padded + // uncompressed form (4 calldata words = 128 bytes: + // x_hi || x_lo || y_hi || y_lo, each coord = 16 zero + // pad bytes + 48 big-endian field bytes) into the + // transcript buffer at `buf_len`. + // + // Matches the patched `Hashable for + // midnight_curves::G1Projective::to_input` in + // midnight-proofs, which now emits the same 128-byte form + // (`midfall/proofs/src/transcript/implementors.rs`). The + // previous emitter hashed the 48-byte ZCash compressed + // encoding instead and ran a 384-bit `lex(y) > lex(p − y)` + // ladder + identity flag fixup to derive the sign bit on + // the fly; switching to the uncompressed form drops that + // ladder entirely. + // + // Canonicality: reject non-zero bytes in the top 16 bytes + // of each `_hi` calldata word and reject coordinates + // outside Fp. Normalizing those bytes before hashing would + // make multiple calldata encodings share one transcript. + // + // This helper does not run an independent curve/subgroup + // check. Instead, ProtocolPlan::validate rejects generated + // plans where an absorbed proof commitment would not later be + // consumed by an EIP-2537 G1MSM or pairing path, and those + // precompiles perform the curve/subgroup validation. + // + // The point's uncompressed form remains in calldata; the + // call site is responsible for `calldatacopy`-ing it into + // memory afterwards if it needs the on-curve coordinates. + function common_uncompressed_g1(buf_len, cptr) -> ret { + let x_hi_word := calldataload(cptr) + let x_lo := calldataload(add(cptr, 0x20)) + let y_hi_word := calldataload(add(cptr, 0x40)) + let y_lo := calldataload(add(cptr, 0x60)) + if shr(128, x_hi_word) { revert(0, 0) } + if shr(128, y_hi_word) { revert(0, 0) } + + let x_hi := and(x_hi_word, 0xffffffffffffffffffffffffffffffff) + let y_hi := and(y_hi_word, 0xffffffffffffffffffffffffffffffff) + if iszero(or(lt(x_hi, BLS_P_HI), and(eq(x_hi, BLS_P_HI), iszero(gt(x_lo, BLS_P_MINUS_ONE_LO))))) { + revert(0, 0) + } + if iszero(or(lt(y_hi, BLS_P_HI), and(eq(y_hi, BLS_P_HI), iszero(gt(y_lo, BLS_P_MINUS_ONE_LO))))) { + revert(0, 0) + } + + // Memcpy the 4 calldata words (128 bytes) verbatim + // into the keccak buffer. + calldatacopy(buf_len, cptr, 0x80) + ret := add(buf_len, 0x80) + } + + // One Keccak finalization + reseed. Returns the new buffer + // cursor (= TRANSCRIPT_MPTR + 32) and stores the squeezed Fq at + // `mptr`. + function squeeze_to(buf_len, mptr) -> ret { + let h0 := keccak256(TRANSCRIPT_MPTR, sub(buf_len, TRANSCRIPT_MPTR)) + // Reseed: write the 32-byte digest at start of buffer. + mstore(TRANSCRIPT_MPTR, h0) + let r := FR_MODULUS + // Sample Fq as uint256(keccak_digest_be) mod r. + mstore(mptr, mod(h0, r)) + ret := add(TRANSCRIPT_MPTR, 32) + } + + // ---------- EC primitives (EIP-2537 wrappers) ---------- + // + // These mirror the BN254 helpers but operate on 4-word G1 + // points. They use planned memory windows above Solidity's + // reserved prefix; the streaming transcript buffer is no longer + // needed once all challenges are squeezed. + + // Invert a contiguous run of Fr words in-place using Montgomery's + // batch inversion trick: + // 1. write prefix products to scratch; + // 2. invert the total product once with modexp; + // 3. walk backward to recover each individual inverse. + // + // The function returns a boolean instead of reverting so callers + // can combine it with other `success` plumbing until a section + // boundary decides whether to fail closed. + function batch_invert(success, mptr_start, mptr_end, scratch_mptr, r) -> ret { + ret := success + if iszero(ret) { leave } + // Memory ranges must be forward and word-aligned by + // construction; a reversed range is always a codegen error. + if lt(mptr_end, mptr_start) { + ret := 0 + leave + } + + let count_bytes := sub(mptr_end, mptr_start) + // Empty batch is valid and leaves memory untouched. + if iszero(count_bytes) { leave } + + // Fast path for a single denominator: avoid prefix scratch and + // just run one modexp inverse in place. + if eq(count_bytes, 0x20) { + let x := mload(mptr_start) + if iszero(x) { + ret := 0 + leave + } + + let single_scratch := scratch_mptr + mstore(add(single_scratch, {{ template_constants.modexp.base_len_offset|hex() }}), {{ template_constants.word_bytes|hex() }}) + mstore(add(single_scratch, {{ template_constants.modexp.exp_len_offset|hex() }}), {{ template_constants.word_bytes|hex() }}) + mstore(add(single_scratch, {{ template_constants.modexp.mod_len_offset|hex() }}), {{ template_constants.word_bytes|hex() }}) + mstore(add(single_scratch, {{ template_constants.modexp.base_offset|hex() }}), x) + mstore(add(single_scratch, {{ template_constants.modexp.exp_offset|hex() }}), sub(r, 2)) + mstore(add(single_scratch, {{ template_constants.modexp.mod_offset|hex() }}), r) + ret := staticcall(gas(), {{ template_constants.modexp.address|hex() }}, single_scratch, {{ template_constants.modexp.frame_bytes|hex() }}, single_scratch, {{ template_constants.modexp.output_bytes|hex() }}) + ret := and(ret, eq(returndatasize(), {{ template_constants.modexp.output_bytes|hex() }})) + if ret { mstore(mptr_start, mload(single_scratch)) } + leave + } + + // Forward pass: scratch stores prefix products up to, but not + // including, the final element. `gp` becomes the total product. + let gp_mptr := scratch_mptr + let gp := mload(mptr_start) + let mptr := add(mptr_start, 0x20) + for {} lt(mptr, sub(mptr_end, 0x20)) {} { + gp := mulmod(gp, mload(mptr), r) + mstore(gp_mptr, gp) + mptr := add(mptr, 0x20) + gp_mptr := add(gp_mptr, 0x20) + } + gp := mulmod(gp, mload(mptr), r) + // A zero total product means at least one denominator was + // zero, so no batch inverse exists. + if iszero(gp) { + ret := 0 + leave + } + + // Invert the total product once. + mstore(add(gp_mptr, {{ template_constants.modexp.base_len_offset|hex() }}), {{ template_constants.word_bytes|hex() }}) + mstore(add(gp_mptr, {{ template_constants.modexp.exp_len_offset|hex() }}), {{ template_constants.word_bytes|hex() }}) + mstore(add(gp_mptr, {{ template_constants.modexp.mod_len_offset|hex() }}), {{ template_constants.word_bytes|hex() }}) + mstore(add(gp_mptr, {{ template_constants.modexp.base_offset|hex() }}), gp) + mstore(add(gp_mptr, {{ template_constants.modexp.exp_offset|hex() }}), sub(r, 2)) + mstore(add(gp_mptr, {{ template_constants.modexp.mod_offset|hex() }}), r) + ret := staticcall(gas(), {{ template_constants.modexp.address|hex() }}, gp_mptr, {{ template_constants.modexp.frame_bytes|hex() }}, gp_mptr, {{ template_constants.modexp.output_bytes|hex() }}) + ret := and(ret, eq(returndatasize(), {{ template_constants.modexp.output_bytes|hex() }})) + let all_inv := mload(gp_mptr) + + // Backward pass: derive each inverse from the inverted total + // product and the saved prefix products. + let first_mptr := mptr_start + let second_mptr := add(first_mptr, 0x20) + gp_mptr := sub(gp_mptr, 0x20) + for {} lt(second_mptr, mptr) {} { + let inv := mulmod(all_inv, mload(gp_mptr), r) + all_inv := mulmod(all_inv, mload(mptr), r) + mstore(mptr, inv) + mptr := sub(mptr, 0x20) + gp_mptr := sub(gp_mptr, 0x20) + } + let inv_first := mulmod(all_inv, mload(second_mptr), r) + let inv_second := mulmod(all_inv, mload(first_mptr), r) + mstore(first_mptr, inv_first) + mstore(second_mptr, inv_second) + } + + // Final EIP-2537 pairing wrapper. `lhs_mptr` and `rhs_mptr` are + // 4-word G1 slots; G2 bases are loaded from the pinned VK payload. + function ec_pairing(success, lhs_mptr, rhs_mptr) -> ret { + ret := success + if iszero(ret) { leave } + // Lay out two (G1, G2) pairs at scratch..scratch+0x300: + // [lhs_g1 (0x80) | G2_BASE (0x100) | rhs_g1 (0x80) | NEG_S_G2_BASE (0x100)] + // Cancun MCOPY (3 + 3·words gas) replaces what used to + // be a 4-step mstore chain for each G1 (~60 gas) and an + // 8-iter mstore loop for each G2 (~240 gas). Net saving + // here is ~500 gas per ec_pairing call. + let scratch := {{ memory.final_pairing_scratch_mptr|hex() }} + mcopy(scratch, lhs_mptr, 0x80) + mcopy(add(scratch, 0x80), G2_BASE_MPTR, 0x100) + mcopy(add(scratch, 0x180), rhs_mptr, 0x80) + mcopy(add(scratch, 0x200), NEG_S_G2_BASE_MPTR, 0x100) + ret := staticcall({{ final_pairing_gas_cap }}, {{ template_constants.eip2537.pairing_address|hex() }}, scratch, {{ template_constants.pairing_two_pair_bytes|hex() }}, scratch, {{ template_constants.word_bytes|hex() }}) + ret := and(ret, eq(returndatasize(), {{ template_constants.word_bytes|hex() }})) + ret := and(ret, mload(scratch)) + if iszero(ret) { revert(0, 0) } + ret := 1 + } diff --git a/proofs/solidity-verifier/templates/partials/verifier/Constants.sol b/proofs/solidity-verifier/templates/partials/verifier/Constants.sol new file mode 100644 index 000000000..69b965f26 --- /dev/null +++ b/proofs/solidity-verifier/templates/partials/verifier/Constants.sol @@ -0,0 +1,177 @@ + {%- match self.expected_vk_codehash %} + {%- when Some with (expected_vk_codehash) %} + /// @notice Verifying-key contract address authorized for this verifier. + /// @dev The runtime length and codehash are pinned by generated constants and checked at construction time. + address public immutable AUTHORIZED_VK; + // Expected VK runtime metadata. The deployed VK runtime is + // INVALID || payload, hence EXPECTED_VK_LENGTH is one byte longer than + // EXPECTED_VK_PAYLOAD_LENGTH. + uint256 internal constant EXPECTED_VK_PAYLOAD_LENGTH = {{ vk_len }}; + uint256 internal constant EXPECTED_VK_LENGTH = {{ vk_len + 1 }}; + uint256 internal constant EXPECTED_VK_CODEHASH_WORD = {{ expected_vk_codehash|hex_padded(64) }}; + bytes32 internal constant EXPECTED_VK_CODEHASH = bytes32(EXPECTED_VK_CODEHASH_WORD); + {%- when None %} + {%- endmatch %} + {%- match quotient_external %} + {%- when Some with (_) %} + /// @notice Quotient evaluator contract authorized for split quotient reconstruction. + /// @dev The evaluator returns the linearization expected scalar and selector buckets; its runtime may be pinned by generated constants. + address public immutable AUTHORIZED_QUOTIENT; + {%- match self.expected_quotient_codehash %} + {%- when Some with (expected_quotient_codehash) %} + // Expected split evaluator runtime metadata. It is checked at deployment + // and again immediately before each external quotient reconstruction. + uint256 internal constant EXPECTED_QUOTIENT_LENGTH = {{ self.expected_quotient_len.unwrap() }}; + uint256 internal constant EXPECTED_QUOTIENT_CODEHASH_WORD = {{ expected_quotient_codehash|hex_padded(64) }}; + bytes32 internal constant EXPECTED_QUOTIENT_CODEHASH = bytes32(EXPECTED_QUOTIENT_CODEHASH_WORD); + {%- when None %} + {%- endmatch %} + {%- when None %} + {%- endmatch %} + + // Solidity ABI calldata cursors. The generated verifier accepts exactly + // verifyProof(bytes proof, uint256[] instances), then parses the `proof` + // bytes itself in the same order as the Rust verifier transcript. + uint256 internal constant PROOF_LEN_CPTR = {{ proof_cptr - 1 }}; + uint256 internal constant PROOF_CPTR = {{ proof_cptr }}; + uint256 internal constant NUM_INSTANCE_CPTR = {{ num_instance_cptr|hex_padded(2) }}; + uint256 internal constant INSTANCE_CPTR = {{ instance_cptr|hex_padded(2) }}; + // First general-purpose memory words reserved by the generated verifier. + // RETURN_MPTR is a single word set to 1 on success. + uint256 internal constant TRANSCRIPT_MPTR = {{ memory.transcript_mptr|hex() }}; + uint256 internal constant RETURN_MPTR = {{ memory.verifier_return_mptr|hex() }}; + + // ---------------------------------------------------------------------- + // Verifying-key memory map. The VK header lives at VK_MPTR, followed + // by the quotient VM payload and commitments. After the full VK + // runtime comes the challenge slots (challenge_mptr..) and the + // per-stage scratch (theta_mptr..). + // ---------------------------------------------------------------------- + uint256 internal constant VK_MPTR = {{ memory.vk_mptr }}; + uint256 internal constant VK_DIGEST_MPTR = {{ memory.vk_mptr + vk_header.vk_digest }}; + uint256 internal constant NUM_INSTANCES_MPTR = {{ memory.vk_mptr + vk_header.num_instances }}; + uint256 internal constant K_MPTR = {{ memory.vk_mptr + vk_header.k }}; + uint256 internal constant N_INV_MPTR = {{ memory.vk_mptr + vk_header.n_inv }}; + uint256 internal constant OMEGA_MPTR = {{ memory.vk_mptr + vk_header.omega }}; + uint256 internal constant OMEGA_INV_MPTR = {{ memory.vk_mptr + vk_header.omega_inv }}; + uint256 internal constant OMEGA_INV_TO_L_MPTR = {{ memory.vk_mptr + vk_header.omega_inv_to_l }}; + uint256 internal constant HAS_ACCUMULATOR_MPTR = {{ memory.vk_mptr + vk_header.has_accumulator }}; + uint256 internal constant ACC_OFFSET_MPTR = {{ memory.vk_mptr + vk_header.acc_offset }}; + uint256 internal constant NUM_ACC_LIMBS_MPTR = {{ memory.vk_mptr + vk_header.num_acc_limbs }}; + uint256 internal constant NUM_ACC_LIMB_BITS_MPTR = {{ memory.vk_mptr + vk_header.num_acc_limb_bits }}; + uint256 internal constant G1_BASE_MPTR = {{ memory.vk_mptr + vk_header.g1_base }}; + uint256 internal constant G2_BASE_MPTR = {{ memory.vk_mptr + vk_header.g2_base }}; + uint256 internal constant NEG_S_G2_BASE_MPTR = {{ memory.vk_mptr + vk_header.neg_s_g2_base }}; + + uint256 internal constant CHALLENGE_MPTR = {{ memory.challenge_mptr }}; + + // Challenge layout. Squeeze order in midnight-proofs: + // user_phase challenges (variable count) + // theta -> beta, gamma -> trash_challenge -> y -> x -> + // x1, x2 -> x3 -> x4 + uint256 internal constant THETA_MPTR = {{ memory.theta_mptr }}; + uint256 internal constant BETA_MPTR = {{ memory.beta_mptr }}; + uint256 internal constant GAMMA_MPTR = {{ memory.gamma_mptr }}; + uint256 internal constant TRASH_CHALLENGE_MPTR = {{ memory.trash_challenge_mptr }}; + uint256 internal constant Y_MPTR = {{ memory.y_mptr }}; + uint256 internal constant X_MPTR = {{ memory.x_mptr }}; + uint256 internal constant X1_MPTR = {{ memory.x1_mptr }}; + uint256 internal constant X2_MPTR = {{ memory.x2_mptr }}; + uint256 internal constant X3_MPTR = {{ memory.x3_mptr }}; + uint256 internal constant X4_MPTR = {{ memory.x4_mptr }}; + + // Batch-open commitments live in 4-word EIP-2537 padded slots. + uint256 internal constant F_COM_MPTR = {{ memory.f_com_mptr }}; + uint256 internal constant PI_MPTR = {{ memory.pi_mptr }}; + + // Accumulator (KZG IVC). + uint256 internal constant ACC_LHS_MPTR = {{ memory.acc_lhs_mptr }}; + uint256 internal constant ACC_RHS_MPTR = {{ memory.acc_rhs_mptr }}; + + // Lagrange / linearization scratch. + uint256 internal constant X_N_MPTR = {{ memory.x_n_mptr }}; + uint256 internal constant X_N_MINUS_1_INV_MPTR = {{ memory.x_n_minus_1_inv_mptr }}; + uint256 internal constant L_LAST_MPTR = {{ memory.l_last_mptr }}; + uint256 internal constant L_BLIND_MPTR = {{ memory.l_blind_mptr }}; + uint256 internal constant L_0_MPTR = {{ memory.l_0_mptr }}; + uint256 internal constant INSTANCE_EVAL_MPTR = {{ memory.instance_eval_mptr }}; + // Legacy name: this is not h(x). It stores the expected opening + // scalar for the linearized commitment, i.e. the negated y-batched + // identity numerator reconstructed from the alleged evals at x. + uint256 internal constant QUOTIENT_EVAL_MPTR = {{ memory.quotient_eval_mptr }}; + uint256 internal constant QUOTIENT_MPTR = {{ memory.quotient_mptr }}; // 4 words + uint256 internal constant F_EVAL_MPTR = {{ memory.f_eval_mptr }}; + uint256 internal constant V_MPTR = {{ memory.v_mptr }}; + uint256 internal constant FINAL_COM_MPTR = {{ memory.final_com_mptr }}; // 4 words + uint256 internal constant PAIRING_LHS_MPTR = {{ memory.pairing_lhs_mptr }}; // 4 words + uint256 internal constant PAIRING_RHS_MPTR = {{ memory.pairing_rhs_mptr }}; // 4 words + + // Multi-prepare scratch (sized at codegen time). + uint256 internal constant ROT_POINTS_MPTR = {{ memory.rot_points_mptr }}; + uint256 internal constant X1_POWERS_MPTR = {{ memory.x1_powers_mptr }}; + // Q_COM materialization is currently fused into the final MSM scratch, + // so this marker intentionally aliases Q_EVAL_SET_MPTR and has zero + // reserved capacity until a future emitter starts writing Q_COM_MPTR. + uint256 internal constant Q_COM_MPTR = {{ memory.q_com_mptr }}; + uint256 internal constant Q_EVAL_SET_MPTR = {{ memory.q_eval_set_mptr }}; + + // Q_EVAL_CPTR is set at runtime once the verifier reaches the q_evals + // block of the proof; we keep it as a memory slot for symmetry. + uint256 internal constant Q_EVAL_CPTR_MPTR = {{ memory.q_eval_cptr_mptr }}; + + // Reserved 4-word slot for the G1 identity (point at infinity) in + // EIP-2537 padded form. EVM memory is zero-initialised, and we + // never write to this region, so the four `mload`s below produce + // 0,0,0,0 which is exactly the identity encoding the EIP-2537 + // ec_add / ec_mul precompiles accept. + uint256 internal constant G1_IDENTITY_MPTR = {{ memory.g1_identity_mptr }}; + + // Decoded polynomial-eval buffer (Optimisation H3). The off-chain + // Solidity proof shim rewrites proof scalars into canonical BE words, + // so `calldataload` gives the field element directly. The transcript- + // side `evaluations` loop range-checks and spills that value here so + // downstream eval references (gate evaluator + PCS q_eval Horner) + // become 3-gas `mload(...)` instead of calldata reads. + uint256 internal constant REVERSED_EVALS_MPTR = {{ memory.reversed_evals_mptr }}; + uint256 internal constant SELECTOR_ACC_MPTR = {{ memory.selector_acc_mptr|hex() }}; + uint256 internal constant QUOTIENT_RETURN_MPTR = {{ memory.quotient_return_mptr|hex() }}; + uint256 internal constant BATCH_INV_SCRATCH_MPTR = {{ memory.batch_invert_scratch_mptr|hex() }}; + uint256 internal constant TRACE_U256_MPTR = {{ memory.trace_u256_mptr|hex() }}; + + // ---------------------------------------------------------------------- + // Per-category bases for EIP-2537 padded G1 commitments. The proof + // calldata carries 128-byte uncompressed/padded G1s after the off-chain + // proof shim repacks midnight-proofs' native compressed stream; this + // region stores the 4-word slots used by PCS / quotient-fold sections. + // + // Cumulative offsets (in words from `comms_mptr_base`): + // ADVICE_COMMS_MPTR_BASE + 0 + // LOOKUP_M_COMMS_MPTR_BASE + 4*total_advices + // PERM_Z_COMMS_MPTR_BASE + 4*total_advices + 4*num_lookups + // LOOKUP_HELPER_COMMS_MPTR_BASE + ... + 4*num_permutation_zs + // LOOKUP_Z_COMMS_MPTR_BASE + ... + 4*lookup_helper_chunks_total + // TRASHCAN_COMMS_MPTR_BASE + ... + 4*num_lookups + // QUOTIENT_LIMB_COMMS_MPTR_BASE + ... + 4*num_trashcans + // ---------------------------------------------------------------------- + uint256 internal constant ADVICE_COMMS_MPTR_BASE = {{ memory.advice_comms_mptr_base }}; + uint256 internal constant LOOKUP_M_COMMS_MPTR_BASE = {{ memory.lookup_m_comms_mptr_base }}; + uint256 internal constant PERM_Z_COMMS_MPTR_BASE = {{ memory.perm_z_comms_mptr_base }}; + uint256 internal constant LOOKUP_HELPER_COMMS_MPTR_BASE = {{ memory.lookup_helper_comms_mptr_base }}; + uint256 internal constant LOOKUP_Z_COMMS_MPTR_BASE = {{ memory.lookup_z_comms_mptr_base }}; + uint256 internal constant TRASHCAN_COMMS_MPTR_BASE = {{ memory.trashcan_comms_mptr_base }}; + uint256 internal constant QUOTIENT_LIMB_COMMS_MPTR_BASE = {{ memory.quotient_limb_comms_mptr_base }}; + + // BLS12-381 scalar-field modulus, used for transcript challenges and all + // Halo2 verifier arithmetic. + uint256 internal constant FR_MODULUS = 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001; + + // BLS12-381 Fp modulus minus one, split like an EIP-2537 coordinate: + // high word = 16 zero bytes || top 16 coordinate bytes, low word = + // bottom 32 coordinate bytes. + uint256 internal constant BLS_P_HI = 0x000000000000000000000000000000001a0111ea397fe69a4b1ba7b6434bacd7; + uint256 internal constant BLS_P_MINUS_ONE_LO = 0x64774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa; + // Packed public-accumulator sentinels for the shifted coordinate codec. + // The `_WITH_ID_FLAG` variant is used only for the first x-coordinate word. + uint256 internal constant BLS_P_MINUS_ONE_PACKED_0 = 0x00000000f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa; + uint256 internal constant BLS_P_MINUS_ONE_PACKED_0_WITH_ID_FLAG = 0x00000000f38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa; + uint256 internal constant BLS_P_MINUS_ONE_PACKED_1 = 0x0000000000000000000000001a0111ea397fe69a4b1ba7b6434bacd764774b84; diff --git a/proofs/solidity-verifier/templates/partials/verifier/Constructors.sol b/proofs/solidity-verifier/templates/partials/verifier/Constructors.sol new file mode 100644 index 000000000..e4de1710f --- /dev/null +++ b/proofs/solidity-verifier/templates/partials/verifier/Constructors.sol @@ -0,0 +1,90 @@ + {%- match self.expected_vk_codehash %} + {%- when Some with (_) %} + {%- match quotient_external %} + {%- when Some with (_) %} + /// @notice Create a verifier pinned to a verifying key and quotient evaluator. + /// @dev Checks EIP-2537 availability and verifies both dependency runtimes before storing their addresses. + /// @param authorizedVk Address of the generated `Halo2VerifyingKey` runtime. + /// @param authorizedQuotient Address of the generated `Halo2QuotientEvaluator` runtime. + constructor(address authorizedVk, address authorizedQuotient) { + // Verifier correctness depends on chain support for the BLS12-381 + // precompiles; fail deployment before pinning any dependency address. + require_eip2537_precompiles(); + // Pin the generated VK runtime exactly. The verifier later repeats the + // codehash/length check before copying the VK payload for a proof. + require( + authorizedVk.code.length == EXPECTED_VK_LENGTH + && authorizedVk.codehash == EXPECTED_VK_CODEHASH, + "invalid vk" + ); + {%- match self.expected_quotient_codehash %} + {%- when Some with (_) %} + // The split evaluator contains generated verifier logic, not a generic + // library. Pin it with the same strictness as the verifying key. + require( + authorizedQuotient.code.length == EXPECTED_QUOTIENT_LENGTH + && authorizedQuotient.codehash == EXPECTED_QUOTIENT_CODEHASH, + "invalid quotient" + ); + {%- when None %} + {%- endmatch %} + // Store the already-validated dependency addresses for proof-time + // memory loading and quotient reconstruction. + AUTHORIZED_VK = authorizedVk; + AUTHORIZED_QUOTIENT = authorizedQuotient; + {%- match self.expected_quotient_codehash %} + {%- when Some with (_) %} + {%- when None %} + {%- endmatch %} + } + {%- when None %} + /// @notice Create a verifier pinned to a generated verifying key. + /// @dev Checks EIP-2537 availability and verifies the VK runtime before storing its address. + /// @param authorizedVk Address of the generated `Halo2VerifyingKey` runtime. + constructor(address authorizedVk) { + // Embedded quotient path: only the external VK runtime needs to be + // pinned, but the curve precompiles are still mandatory. + require_eip2537_precompiles(); + require( + authorizedVk.code.length == EXPECTED_VK_LENGTH + && authorizedVk.codehash == EXPECTED_VK_CODEHASH, + "invalid vk" + ); + AUTHORIZED_VK = authorizedVk; + } + {%- endmatch %} + {%- when None %} + {%- match quotient_external %} + {%- when Some with (_) %} + /// @notice Create a verifier pinned to a quotient evaluator. + /// @dev Used when the VK is embedded in the verifier but quotient reconstruction is split out. + /// @param authorizedQuotient Address of the generated `Halo2QuotientEvaluator` runtime. + constructor(address authorizedQuotient) { + // Embedded VK path: the verifier bytecode carries the VK payload, but + // the external quotient evaluator remains a pinned dependency. + require_eip2537_precompiles(); + {%- match self.expected_quotient_codehash %} + {%- when Some with (_) %} + require( + authorizedQuotient.code.length == EXPECTED_QUOTIENT_LENGTH + && authorizedQuotient.codehash == EXPECTED_QUOTIENT_CODEHASH, + "invalid quotient" + ); + {%- when None %} + {%- endmatch %} + AUTHORIZED_QUOTIENT = authorizedQuotient; + {%- match self.expected_quotient_codehash %} + {%- when Some with (_) %} + {%- when None %} + {%- endmatch %} + } + {%- when None %} + /// @notice Create a verifier with embedded verifier data. + /// @dev Checks EIP-2537 availability at deployment. + constructor() { + // Fully embedded verifier: no external generated dependency exists, + // so deployment only checks precompile availability. + require_eip2537_precompiles(); + } + {%- endmatch %} + {%- endmatch %} diff --git a/proofs/solidity-verifier/templates/partials/verifier/FinalPairing.yul b/proofs/solidity-verifier/templates/partials/verifier/FinalPairing.yul new file mode 100644 index 000000000..fd7e2a747 --- /dev/null +++ b/proofs/solidity-verifier/templates/partials/verifier/FinalPairing.yul @@ -0,0 +1,83 @@ + // Batch the prevalidated public IVC accumulator pairing equation + // into the final KZG pairing. + // + // We do not simply multiply the two pairing equations together: + // two bad equations could cancel. Instead, after all four G1 + // pairing inputs are fixed, derive a verifier-local randomizer + // alpha and check: + // + // e(kzg_rhs + alpha * acc_rhs, G2_BASE) + // * e(kzg_lhs + alpha * acc_lhs, NEG_S_G2_BASE) == 1 + // + // If either original equation is bad, this combined equation + // holds for at most one alpha in Fr. + {%- if self.expected_has_accumulator %} + { + let batch_ptr := {{ memory.accumulator_pairing_batch_mptr|hex() }} + + // Domain || KZG rhs/lhs || accumulator rhs/lhs. + mstore(batch_ptr, {{ template_constants.accumulator.pairing_batch_domain_tag_hex }}) + mcopy(add(batch_ptr, {{ template_constants.accumulator.pairing_batch_rhs_offset|hex() }}), PAIRING_RHS_MPTR, {{ template_constants.g1_bytes|hex() }}) + mcopy(add(batch_ptr, {{ template_constants.accumulator.pairing_batch_lhs_offset|hex() }}), PAIRING_LHS_MPTR, {{ template_constants.g1_bytes|hex() }}) + mcopy(add(batch_ptr, {{ template_constants.accumulator.pairing_batch_acc_rhs_offset|hex() }}), ACC_RHS_MPTR, {{ template_constants.g1_bytes|hex() }}) + mcopy(add(batch_ptr, {{ template_constants.accumulator.pairing_batch_acc_lhs_offset|hex() }}), ACC_LHS_MPTR, {{ template_constants.g1_bytes|hex() }}) + // alpha is Fiat-Shamir over the fully materialized pairing + // inputs. Replace the negligible zero draw with one so the + // accumulator equation cannot be accidentally dropped. + let acc_pair_alpha := mod(keccak256(batch_ptr, {{ template_constants.accumulator.pairing_batch_hash_bytes|hex() }}), r) + if iszero(acc_pair_alpha) { acc_pair_alpha := 1 } + + // PAIRING_RHS_MPTR += alpha * ACC_RHS_MPTR. + // First compute alpha * ACC_RHS with a one-pair G1MSM, then + // add it into the KZG RHS point. + mcopy(batch_ptr, ACC_RHS_MPTR, {{ template_constants.g1_bytes|hex() }}) + mstore(add(batch_ptr, {{ template_constants.g1_bytes|hex() }}), acc_pair_alpha) + if success { + success := staticcall({{ g1msm_single_gas_cap }}, {{ template_constants.eip2537.g1msm_address|hex() }}, batch_ptr, {{ template_constants.g1_msm_pair_bytes|hex() }}, batch_ptr, {{ template_constants.g1_bytes|hex() }}) + success := and(success, eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) + } + mcopy(add(batch_ptr, {{ template_constants.g1_bytes|hex() }}), PAIRING_RHS_MPTR, {{ template_constants.g1_bytes|hex() }}) + if success { + success := staticcall({{ template_constants.eip2537.g1add_gas_cap }}, {{ template_constants.eip2537.g1add_address|hex() }}, batch_ptr, {{ template_constants.g1add_input_bytes|hex() }}, PAIRING_RHS_MPTR, {{ template_constants.g1_bytes|hex() }}) + success := and(success, eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) + } + + // PAIRING_LHS_MPTR += alpha * ACC_LHS_MPTR. + // Mirror the same randomized batching on the KZG LHS point. + mcopy(batch_ptr, ACC_LHS_MPTR, {{ template_constants.g1_bytes|hex() }}) + mstore(add(batch_ptr, {{ template_constants.g1_bytes|hex() }}), acc_pair_alpha) + if success { + success := staticcall({{ g1msm_single_gas_cap }}, {{ template_constants.eip2537.g1msm_address|hex() }}, batch_ptr, {{ template_constants.g1_msm_pair_bytes|hex() }}, batch_ptr, {{ template_constants.g1_bytes|hex() }}) + success := and(success, eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) + } + mcopy(add(batch_ptr, {{ template_constants.g1_bytes|hex() }}), PAIRING_LHS_MPTR, {{ template_constants.g1_bytes|hex() }}) + if success { + success := staticcall({{ template_constants.eip2537.g1add_gas_cap }}, {{ template_constants.eip2537.g1add_address|hex() }}, batch_ptr, {{ template_constants.g1add_input_bytes|hex() }}, PAIRING_LHS_MPTR, {{ template_constants.g1_bytes|hex() }}) + success := and(success, eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) + } + } + {%- endif %} + + {%- if self.gas_checkpoints %} + gas_checkpoint(15) // after public accumulator pairing batch prep (omitted for no-accumulator VKs) + {%- endif %} + + // The Yul `ec_pairing` helper checks + // e(arg0, G2_BASE) * e(arg1, NEG_S_G2_BASE) == 1 + // i.e. e(arg0, [1]_2) = e(arg1, [s]_2). + // + // The KZG pairing identity is + // e(final_com - v*G + x3*pi, [1]_2) = e(pi, [s]_2), + // so arg0 must be (final_com - v*G + x3*pi) and arg1 must be + // pi. The PAIRING_*_MPTR slots store + // PAIRING_LHS_MPTR := pi + // PAIRING_RHS_MPTR := final_com - v*G + x3*pi + // -- the historical "LHS"/"RHS" naming follows the dual MSM + // accumulator (left = pi, right = combined) and *not* the + // pairing argument order. Pass them swapped to ec_pairing. + if iszero(success) { revert(0, 0) } + success := ec_pairing(success, PAIRING_RHS_MPTR, PAIRING_LHS_MPTR) + + {%- if self.gas_checkpoints %} + gas_checkpoint(16) // after final ec_pairing + {%- endif %} diff --git a/proofs/solidity-verifier/templates/partials/verifier/Lagrange.yul b/proofs/solidity-verifier/templates/partials/verifier/Lagrange.yul new file mode 100644 index 000000000..6eb05fce4 --- /dev/null +++ b/proofs/solidity-verifier/templates/partials/verifier/Lagrange.yul @@ -0,0 +1,89 @@ + // =============================================================== + // Lagrange & instance-evaluation block (pure Fr arithmetic). + // =============================================================== + { + let k := {{ k }} + let x := mload(X_MPTR) + // Compute x^n by repeated squaring, with n = 2^k. + let x_n := x + for { let idx := 0 } lt(idx, k) { idx := add(idx, 1) } { + x_n := mulmod(x_n, x_n, r) + } + + let omega := mload(OMEGA_MPTR) + + // First pass writes denominators (x - omega_i) for every + // Lagrange value needed below, then appends x^n - 1. The + // batch inversion pass turns all of them into inverses in one + // modexp call. + let mptr := X_N_MPTR + let mptr_end := add(mptr, {{ ((num_instances + num_neg_lagranges) * 32)|hex() }}) + {%- if num_instances == 0 %} + // No public instances still need one denominator slot so + // L_0 can be recovered for boundary identities. + mptr_end := add(mptr_end, 0x20) + {%- endif %} + for { let pow_of_omega := mload(OMEGA_INV_TO_L_MPTR) } + lt(mptr, mptr_end) + { mptr := add(mptr, 0x20) } { + mstore(mptr, addmod(x, sub(r, pow_of_omega), r)) + pow_of_omega := mulmod(pow_of_omega, omega, r) + } + let x_n_minus_1 := addmod(x_n, sub(r, 1), r) + mstore(mptr_end, x_n_minus_1) + success := batch_invert(success, X_N_MPTR, add(mptr_end, 0x20), BATCH_INV_SCRATCH_MPTR, r) + + // Convert inverted denominators into Lagrange evaluations: + // L_i(x) = (x^n - 1) * n^-1 * omega_i / (x - omega_i). + mptr := X_N_MPTR + let l_i_common := mulmod(x_n_minus_1, mload(N_INV_MPTR), r) + for { let pow_of_omega := mload(OMEGA_INV_TO_L_MPTR) } + lt(mptr, mptr_end) + { mptr := add(mptr, 0x20) } { + mstore(mptr, mulmod(l_i_common, mulmod(mload(mptr), pow_of_omega, r), r)) + pow_of_omega := mulmod(pow_of_omega, omega, r) + } + + // l_blind is the sum of the negative-rotation Lagrange terms + // used by the midnight-proofs blinding identity. + let l_blind := mload(add(X_N_MPTR, 0x20)) + let l_i_cptr := add(X_N_MPTR, 0x40) + for { let l_i_cptr_end := add(X_N_MPTR, {{ (num_neg_lagranges * 32)|hex() }}) } + lt(l_i_cptr, l_i_cptr_end) + { l_i_cptr := add(l_i_cptr, 0x20) } { + l_blind := addmod(l_blind, mload(l_i_cptr), r) + } + + // Public instance polynomial evaluation at x. Instance words + // have already been range-checked and absorbed in transcript + // order; this loop only forms the linear combination. + let instance_eval := 0 + for { + let instance_cptr := INSTANCE_CPTR + let instance_cptr_end := add(instance_cptr, {{ (num_instances * 32)|hex() }}) + } + lt(instance_cptr, instance_cptr_end) + { instance_cptr := add(instance_cptr, 0x20) + l_i_cptr := add(l_i_cptr, 0x20) } { + instance_eval := addmod(instance_eval, mulmod(mload(l_i_cptr), calldataload(instance_cptr), r), r) + } + + // Persist the derived values into named memory slots consumed + // by quotient reconstruction and PCS preparation. + let x_n_minus_1_inv := mload(mptr_end) + let l_last := mload(X_N_MPTR) + let l_0 := mload(add(X_N_MPTR, {{ (num_neg_lagranges * 32)|hex() }})) + + mstore(X_N_MPTR, x_n) + mstore(X_N_MINUS_1_INV_MPTR, x_n_minus_1_inv) + mstore(L_LAST_MPTR, l_last) + mstore(L_BLIND_MPTR, l_blind) + mstore(L_0_MPTR, l_0) + mstore(INSTANCE_EVAL_MPTR, instance_eval) + } + + {%- if self.gas_checkpoints %} + gas_checkpoint(11) // after Lagrange + instance evaluation block + {%- endif %} + + if iszero(success) { revert(0, 0) } diff --git a/proofs/solidity-verifier/templates/partials/verifier/Pcs.yul b/proofs/solidity-verifier/templates/partials/verifier/Pcs.yul new file mode 100644 index 000000000..ffce58961 --- /dev/null +++ b/proofs/solidity-verifier/templates/partials/verifier/Pcs.yul @@ -0,0 +1,39 @@ + // =============================================================== + // PCS computation (multi-prepare emitter from Step 5). + // + // The Rust lowering stage has already expanded the KZG multi-open + // equation into a sequence of generated Yul sub-blocks. Those + // blocks populate: + // - F_EVAL_MPTR / V_MPTR scalar batching values; + // - FINAL_COM_MPTR for the fused commitment MSM; + // - PAIRING_LHS_MPTR and PAIRING_RHS_MPTR for the final pairing. + // =============================================================== + { + {%- for code_block in pcs_computations %} + // Generated PCS sub-block {{ loop.index }}. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + {%- for line in code_block %} + {{ line }} + {%- endfor %} + } + {%- if self.gas_checkpoints && !loop.last %} + gas_checkpoint({{ 17 + loop.index0 }}) // after PCS sub-block {{ loop.index }} + {%- endif %} + {%- endfor %} + } + + {%- if self.gas_checkpoints %} + gas_checkpoint(14) // after PCS computation block (= sub-block 6) + {%- endif %} + + {%- if self.trace %} + // Trace the final pairing inputs and selector accumulator buckets + // after PCS expansion, matching the Rust verifier trace labels. + trace_point(27, PAIRING_LHS_MPTR) + trace_point(28, PAIRING_RHS_MPTR) + {%- for _ in simple_selector_cols %} + trace_u256({{ selector_trace_base + loop.index0 }}, mload(add(SELECTOR_ACC_MPTR, {{ (loop.index0 * 32)|hex() }}))) + {%- endfor %} + {%- endif %} diff --git a/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol b/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol new file mode 100644 index 000000000..ea824e2e9 --- /dev/null +++ b/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol @@ -0,0 +1,37 @@ + /// @notice Smoke-check the BLS12-381 precompiles required by the verifier. + /// @dev Uses identity inputs to catch absent EIP-2537 implementations, short return data, and incompatible pairing semantics at deployment. + function require_eip2537_precompiles() private view { + assembly ("memory-safe") { + // Scratch is reused for every precompile probe. Start with the + // EIP-2537 identity encoding for G1/G2: all-zero padded words. + let scratch := {{ memory.constructor_smoke_scratch_mptr|hex() }} + for { let off := 0 } lt(off, {{ template_constants.eip2537.smoke_scratch_bytes|hex() }}) { off := add(off, {{ template_constants.word_bytes|hex() }}) } { + mstore(add(scratch, off), 0) + } + + // G1ADD(identity, identity) -> identity, 128-byte return. + // This catches chains where the precompile is missing or returns a + // non-standard success shape. + if iszero(staticcall({{ template_constants.eip2537.g1add_gas_cap }}, {{ template_constants.eip2537.g1add_address|hex() }}, scratch, {{ template_constants.g1add_input_bytes|hex() }}, scratch, {{ template_constants.g1_bytes|hex() }})) { revert(0, 0) } + if iszero(eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) { revert(0, 0) } + if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { + revert(0, 0) + } + + // G1MSM([(identity, 0)]) -> identity, 128-byte return. + // The production verifier uses G1MSM both for commitments and as + // the subgroup validator for absorbed proof points. + if iszero(staticcall({{ template_constants.eip2537.g1msm_smoke_gas_cap }}, {{ template_constants.eip2537.g1msm_address|hex() }}, scratch, {{ template_constants.g1_msm_pair_bytes|hex() }}, scratch, {{ template_constants.g1_bytes|hex() }})) { revert(0, 0) } + if iszero(eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) { revert(0, 0) } + if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { + revert(0, 0) + } + + // PAIRING_CHECK([(identity_g1, identity_g2)]) -> true, + // 32-byte return. This catches absent pairing precompiles, + // short return data, and obviously incompatible semantics. + if iszero(staticcall({{ template_constants.eip2537.pairing_smoke_gas_cap }}, {{ template_constants.eip2537.pairing_address|hex() }}, scratch, {{ template_constants.pairing_pair_bytes|hex() }}, scratch, {{ template_constants.word_bytes|hex() }})) { revert(0, 0) } + if iszero(eq(returndatasize(), {{ template_constants.word_bytes|hex() }})) { revert(0, 0) } + if iszero(eq(mload(scratch), 1)) { revert(0, 0) } + } + } diff --git a/proofs/solidity-verifier/templates/partials/verifier/QuotientAndLinearization.yul b/proofs/solidity-verifier/templates/partials/verifier/QuotientAndLinearization.yul new file mode 100644 index 000000000..dfdd1a347 --- /dev/null +++ b/proofs/solidity-verifier/templates/partials/verifier/QuotientAndLinearization.yul @@ -0,0 +1,133 @@ + {%- match quotient_external %} + {%- when Some with (qext) %} + // =============================================================== + // External batched identity numerator reconstruction. + // + // The quotient evaluator receives the verifier memory image from + // QUOTIENT_FRAME_BASE..+QUOTIENT_FRAME_LEN, reconstructs the same + // y-batched numerator, and returns: + // word 0: magic/version + // word 1: linearization expected eval + // word 2..: simple-selector accumulators + // =============================================================== + { + let q_out := QUOTIENT_RETURN_MPTR + {%- match self.expected_quotient_codehash %} + {%- when Some with (_) %} + // The quotient evaluator is as correctness-critical as the VK: + // it reconstructs the y-batched identity numerator and + // selector buckets. Re-check the pinned runtime before every + // external call, mirroring the VK freshness guard above. + if iszero(and( + eq(extcodesize(quotientEvaluator), EXPECTED_QUOTIENT_LENGTH), + eq(extcodehash(quotientEvaluator), EXPECTED_QUOTIENT_CODEHASH_WORD) + )) { revert(0, 0) } + {%- when None %} + {%- endmatch %} + {%- if self.trace %} + if iszero(call(gas(), quotientEvaluator, 0, {{ qext.frame_base|hex() }}, {{ qext.frame_len|hex() }}, q_out, {{ qext.output_len|hex() }})) { revert(0, 0) } + {%- else %} + if iszero(staticcall(gas(), quotientEvaluator, {{ qext.frame_base|hex() }}, {{ qext.frame_len|hex() }}, q_out, {{ qext.output_len|hex() }})) { revert(0, 0) } + {%- endif %} + if iszero(eq(returndatasize(), {{ qext.output_len|hex() }})) { revert(0, 0) } + if iszero(eq(mload(q_out), {{ qext.magic|hex_padded(64) }})) { revert(0, 0) } + // Word 1 is the negated y-batched identity numerator, stored + // in the same memory slot used by the monolithic path. + mstore(QUOTIENT_EVAL_MPTR, mload(add(q_out, 0x20))) + {%- if simple_selector_cols.len() > 0 %} + // Remaining return words are selector linearization buckets. + // Copy them back into the canonical selector accumulator region + // so the PCS code path is identical for split and monolithic + // quotient renders. + for { let q_i := 0 } lt(q_i, {{ simple_selector_cols.len() }}) { q_i := add(q_i, 1) } { + mstore(add(SELECTOR_ACC_MPTR, shl(5, q_i)), mload(add(q_out, add(0x40, shl(5, q_i))))) + } + {%- endif %} + } + {%- when None %} + {%- include "partials/quotient_numerator/QuotientHelpers.yul" %} + {%- include "partials/quotient_numerator/QuotientNumeratorBlock.yul" %} + {%- endmatch %} + + {%- if self.gas_checkpoints %} + gas_checkpoint(12) // after batched identity numerator reconstruction + {%- endif %} + + // =============================================================== + // Prepare linearization scalars for the final PCS MSM. + // + // The linearized commitment is + // (1 - x^n) * Σ_i x_split^i * Q_i + // + Σ_j sel_acc_j * S_j_com, + // where x_split = x^(n-1). Instead of materializing that point + // with a standalone G1MSM here, PCS block 5 expands the + // linearized commitment into its quotient and selector + // pairs inside the already-fused final MSM. + // + // QUOTIENT_MPTR is no longer a G1 point in this path. Its first + // two words carry: + // word 0: x_split + // word 1: one_minus_x_n + // =============================================================== + { + let x := mload(X_MPTR) + let k := {{ k }} + // Compute both x^n and x^(n-1) with the same squaring walk: + // x_pow_2i tracks x^(2^i), while x_pow_2i_minus1 tracks + // x^(2^i - 1). + let x_pow_2i := x + let x_pow_2i_minus1 := 1 + for { let idx := 0 } lt(idx, k) { idx := add(idx, 1) } { + x_pow_2i_minus1 := mulmod( + mulmod(x_pow_2i_minus1, x_pow_2i_minus1, r), + x, + r + ) + x_pow_2i := mulmod(x_pow_2i, x_pow_2i, r) + } + let x_split := x_pow_2i_minus1 + let one_minus_x_n := addmod(1, sub(r, x_pow_2i), r) + + // PCS block 5 interprets this 2-word payload as scalar + // metadata, not as a materialized G1 point. + mstore(QUOTIENT_MPTR, x_split) + mstore(add(QUOTIENT_MPTR, 0x20), one_minus_x_n) + } + + {%- if self.trace %} + // Materialize the linearization commitment for trace comparison. + // The production path expands the same terms directly into the + // fused final PCS MSM below. + { + let lin_scratch := add(SELECTOR_ACC_MPTR, {{ (simple_selector_cols.len() * 0x20)|hex() }}) + let lin_pair := lin_scratch + let lin_cur_scalar := mload(add(QUOTIENT_MPTR, 0x20)) + {%- for _ in 0..num_quotients %} + // Trace-only quotient limb term: Q_i * ((1 - x^n) * x_split^i). + mcopy(lin_pair, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, {{ (loop.index0 * 0x80)|hex() }}), 0x80) + mstore(add(lin_pair, 0x80), lin_cur_scalar) + lin_pair := add(lin_pair, 0xa0) + {%- if !loop.last %} + lin_cur_scalar := mulmod(lin_cur_scalar, mload(QUOTIENT_MPTR), r) + {%- endif %} + {%- endfor %} + {%- for col in simple_selector_cols %} + // Trace-only selector term: fixed selector commitment times + // the generated selector accumulator bucket. + mcopy(lin_pair, {{ (fixed_comm_mptr + col * 0x80)|hex() }}, 0x80) + mstore(add(lin_pair, 0x80), mload(add(SELECTOR_ACC_MPTR, {{ (loop.index0 * 0x20)|hex() }}))) + lin_pair := add(lin_pair, 0xa0) + {%- endfor %} + let lin_trace_ok := staticcall({{ lin_trace_g1msm_gas_cap }}, {{ template_constants.eip2537.g1msm_address|hex() }}, lin_scratch, {{ ((num_quotients + simple_selector_cols.len()) * template_constants.g1_msm_pair_bytes)|hex() }}, lin_scratch, {{ template_constants.g1_bytes|hex() }}) + lin_trace_ok := and(lin_trace_ok, eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) + if iszero(lin_trace_ok) { + mstore(TRACE_U256_MPTR, 34) + revert(TRACE_U256_MPTR, 0x20) + } + trace_point(34, lin_scratch) + } + {%- endif %} + + {%- if self.gas_checkpoints %} + gas_checkpoint(13) // after linearization scalar prep + {%- endif %} diff --git a/proofs/solidity-verifier/templates/partials/verifier/TraceAndGasHelpers.yul b/proofs/solidity-verifier/templates/partials/verifier/TraceAndGasHelpers.yul new file mode 100644 index 000000000..7a4aaad65 --- /dev/null +++ b/proofs/solidity-verifier/templates/partials/verifier/TraceAndGasHelpers.yul @@ -0,0 +1,23 @@ + {%- if self.trace %} + // Trace builds emit u256 values through a planned scratch slot. + // The topic is the stable trace ID used by Rust/Solidity fixture + // comparison; the data word is the observed scalar. + function trace_u256(id, value) { + mstore(TRACE_U256_MPTR, value) + log1(TRACE_U256_MPTR, 0x20, id) + } + // Trace G1 points as their 128-byte EIP-2537 memory slot. + function trace_point(id, mptr) { + log1(mptr, 0x80, id) + } + {%- endif %} + {%- if self.gas_checkpoints %} + // Section-boundary gas-attribution checkpoint. Emits a + // single LOG1 (no data) with topic = (id << 248) | gas(). + // Cost: 375 (LOG base) + 375 (1 topic) = 750 gas/call. + // Host-side parses the topic into (id, gas_left) and prints + // pairwise deltas (see `dump_gas_checkpoints`). + function gas_checkpoint(id) { + log1(0, 0, or(shl(248, id), gas())) + } + {%- endif %} diff --git a/proofs/solidity-verifier/templates/partials/verifier/TraceReturn.yul b/proofs/solidity-verifier/templates/partials/verifier/TraceReturn.yul new file mode 100644 index 000000000..5f040942c --- /dev/null +++ b/proofs/solidity-verifier/templates/partials/verifier/TraceReturn.yul @@ -0,0 +1,58 @@ + {%- if self.trace %} + // Terminal trace emission. IDs are intentionally stable across + // generated verifiers so fixture tooling can diff Rust and + // Solidity executions without understanding a particular circuit. + trace_u256(1, mload(VK_DIGEST_MPTR)) + trace_u256(2, {{ num_instances }}) + trace_u256(3, {{ k }}) + trace_u256(4, mload(N_INV_MPTR)) + trace_u256(5, mload(OMEGA_MPTR)) + trace_u256(6, mload(OMEGA_INV_MPTR)) + {%- for phase in user_phases %} + {%- for j in 0..phase.num_challenges %} + trace_u256({{ 1000 + phase.challenge_offset + j }}, mload(add(CHALLENGE_MPTR, {{ ((phase.challenge_offset + j) * 32)|hex() }}))) + {%- endfor %} + {%- endfor %} + trace_u256(7, mload(THETA_MPTR)) + trace_u256(8, mload(BETA_MPTR)) + trace_u256(9, mload(GAMMA_MPTR)) + trace_u256(10, mload(Y_MPTR)) + trace_u256(11, mload(X_MPTR)) + {%- if num_trashcans != 0 %} + trace_u256(12, mload(TRASH_CHALLENGE_MPTR)) + {%- endif %} + trace_u256(13, mload(X1_MPTR)) + trace_u256(14, mload(X2_MPTR)) + trace_u256(15, mload(X3_MPTR)) + trace_u256(16, mload(X4_MPTR)) + trace_u256(17, mload(X_N_MPTR)) + trace_u256(18, mload(X_N_MINUS_1_INV_MPTR)) + trace_u256(19, mload(L_LAST_MPTR)) + trace_u256(20, mload(L_BLIND_MPTR)) + trace_u256(21, mload(L_0_MPTR)) + trace_u256(22, mload(INSTANCE_EVAL_MPTR)) + trace_u256(23, mload(QUOTIENT_EVAL_MPTR)) + // Rust traces often print the positive numerator; the verifier + // stores the negated expected scalar for the linearized opening. + trace_u256(36, addmod(0, sub(r, mload(QUOTIENT_EVAL_MPTR)), r)) + // QUOTIENT_MPTR carries scalar metadata in the fused production + // path. Zero the unused point words before tracing it as a G1 slot. + mstore(add(QUOTIENT_MPTR, 0x40), 0) + mstore(add(QUOTIENT_MPTR, 0x60), 0) + trace_point(24, QUOTIENT_MPTR) + trace_point(25, F_COM_MPTR) + trace_point(26, PI_MPTR) + trace_u256(31, mload(F_EVAL_MPTR)) + trace_u256(32, mload(V_MPTR)) + trace_point(33, FINAL_COM_MPTR) + trace_u256(35, success) + {%- if self.expected_has_accumulator %} + trace_point(29, ACC_LHS_MPTR) + trace_point(30, ACC_RHS_MPTR) + {%- endif %} + {%- endif %} + + // Success path is terminal. Invalid inputs have already reverted, + // so the Solidity ABI observes `true`. + mstore(RETURN_MPTR, 1) + return(RETURN_MPTR, 0x20) diff --git a/proofs/solidity-verifier/templates/partials/verifier/TranscriptProofParser.yul b/proofs/solidity-verifier/templates/partials/verifier/TranscriptProofParser.yul new file mode 100644 index 000000000..ce80d5964 --- /dev/null +++ b/proofs/solidity-verifier/templates/partials/verifier/TranscriptProofParser.yul @@ -0,0 +1,423 @@ + // =============================================================== + // Transcript: VK digest + instances + proof. + // + // This block is the Solidity mirror of the native Midfall verifier + // transcript schedule. It does three jobs at once: + // + // 1. Absorb public data and proof bytes into the streaming + // Keccak transcript in exactly the native order. + // 2. Decode/range-check proof scalars and canonical G1 calldata. + // 3. Copy proof commitments/evaluations into planned memory + // slots consumed by Lagrange, quotient, PCS, and pairing + // blocks later in the verifier. + // + // `buf_len` is a write cursor into the transcript buffer. The + // helper functions append bytes and return the new cursor; squeeze + // helpers hash memory[TRANSCRIPT_MPTR..buf_len), reseed the buffer + // with the digest, and write the sampled Fr challenge to memory. + // =============================================================== + let buf_len := transcript_init() + // VK_DIGEST_MPTR holds the digest as a BE 32-byte word (the + // VK contract stores it via `mstore`, which matches the + // Keccak Fq transcript input). + // + // This digest commits to the verifier key / constraint system + // before any proof material is read. + buf_len := common_word(buf_len, mload(VK_DIGEST_MPTR)) + + // Absorb committed_pi = G1Affine::identity() when the + // `committed-instances` feature is on in midnight-proofs. + // Under the patched `Hashable::to_input` (see + // `midfall/proofs/src/transcript/implementors.rs`), the + // identity hashes as 128 zero bytes (EIP-2537 (0,0) + // convention), NOT the 48-byte ZCash compressed form + // 0xc0||47*0x00 that the previous emitter produced. + // Native verifier absorbs this BEFORE the instance count. + { + // 128 zero bytes: zero out 4 consecutive 32-byte words + // at buf_len. + // This is a raw transcript absorb, not a memory slot kept for + // later elliptic-curve operations. + mstore(buf_len, 0) + mstore(add(buf_len, 0x20), 0) + mstore(add(buf_len, 0x40), 0) + mstore(add(buf_len, 0x60), 0) + buf_len := add(buf_len, 0x80) + } + + { + // Native verifier absorbs a length scalar before instance + // values; Keccak Fq transcript input is canonical BE. + // The ABI length was already checked against this generated + // constant in VkLoading.yul. + buf_len := common_word(buf_len, {{ num_instances }}) + + let instance_cptr := INSTANCE_CPTR + for { let instance_cptr_end := add(instance_cptr, {{ (num_instances * 32)|hex() }}) } + lt(instance_cptr, instance_cptr_end) + { instance_cptr := add(instance_cptr, 0x20) } { + let inst_be := calldataload(instance_cptr) + // Public inputs are BLS12-381 scalar-field elements. They + // must be canonical before transcript absorption; accepting + // non-canonical encodings would admit transcript aliases. + success := and(success, lt(inst_be, r)) + // Instances are passed BE in calldata, matching the + // Keccak Fq transcript input. + buf_len := common_word(buf_len, inst_be) + } + } + + {%- if self.gas_checkpoints %} + gas_checkpoint(3) // after VK digest + committed_pi + instance absorbs + {%- endif %} + + // =============================================================== + // Per-user-phase reads + challenge squeezes. + // + // Each proof G1 is already EIP-2537 padded in calldata. The + // verifier validates and absorbs that 128-byte form, then copies + // it into the corresponding per-category MPTR. The PCS / + // quotient-fold blocks below dereference those MPTRs. + // + // All G1 reads follow the same pattern: + // - common_uncompressed_g1 canonicalizes/range-checks the two Fp + // coordinates and appends the exact 128 calldata bytes; + // - calldatacopy stores the same 4-word G1 slot in planned + // memory for later EIP-2537 precompile calls; + // - proof_cptr advances by one G1 byte length. + // =============================================================== + // proof_cptr walks the raw proof bytes inside the ABI `bytes` + // payload. Every successful read advances it exactly once, and the + // final equality check below proves the parser consumed the whole + // generated proof layout. + let proof_cptr := PROOF_CPTR + // advice_walk mirrors proof commitment order into the contiguous + // G1 commitment memory region used by PCS and quotient folding. + let advice_walk := ADVICE_COMMS_MPTR_BASE + {%- if self.trace %} + // Trace IDs are monotonic across all commitments/evaluations in + // transcript order, matching the Rust trace comparison harness. + let proof_commit_trace_id := {{ proof_commit_trace_base }} + let proof_eval_trace_id := {{ proof_eval_trace_base }} + {%- endif %} + + {%- for phase in user_phases %} + // ---- User phase {{ loop.index }} ---- + // Advice commitments for this phase are absorbed before the phase's + // challenge squeezes. The number of commitments and challenges is + // generated from the protocol plan. + for { let end := add(proof_cptr, {{ phase.advice_bytes|hex() }}) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + // Store the commitment at its phase-ordered advice slot. + calldatacopy(advice_walk, proof_cptr, {{ template_constants.g1_bytes|hex() }}) + {%- if self.trace %} + trace_point(proof_commit_trace_id, advice_walk) + proof_commit_trace_id := add(proof_commit_trace_id, 1) + {%- endif %} + advice_walk := add(advice_walk, {{ template_constants.g1_bytes|hex() }}) + proof_cptr := add(proof_cptr, {{ template_constants.g1_bytes|hex() }}) + } + {%- for j in 0..phase.num_challenges %} + // User-phase challenge {{ j }} for phase {{ loop.index }}. The + // challenge slots are contiguous under CHALLENGE_MPTR, but each + // phase owns a generated offset. + buf_len := squeeze_to(buf_len, add(CHALLENGE_MPTR, {{ ((phase.challenge_offset + j) * 32)|hex() }})) + {%- endfor %} + {%- endfor %} + + {%- if self.gas_checkpoints %} + gas_checkpoint(4) // after user-phase advice reads + user challenge squeezes + {%- endif %} + + // ---- theta ---- + // From this point onward the transcript alternates between + // squeezed challenges and proof commitments exactly as + // midnight-proofs does in `plonk/verifier.rs`. + // theta batches lookup input expressions. + buf_len := squeeze_to(buf_len, THETA_MPTR) + + {%- if num_lookups != 0 %} + // ---- multiplicities (one G1 per lookup) ---- + // Lookup multiplicity commitments are absorbed after theta and + // copied into their own contiguous G1 region. + let lookup_m_walk := LOOKUP_M_COMMS_MPTR_BASE + for { let end := add(proof_cptr, {{ codegen_layout.proof.lookup_multiplicities.byte_len|hex() }}) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(lookup_m_walk, proof_cptr, {{ template_constants.g1_bytes|hex() }}) + {%- if self.trace %} + trace_point(proof_commit_trace_id, lookup_m_walk) + proof_commit_trace_id := add(proof_commit_trace_id, 1) + {%- endif %} + lookup_m_walk := add(lookup_m_walk, {{ template_constants.g1_bytes|hex() }}) + proof_cptr := add(proof_cptr, {{ template_constants.g1_bytes|hex() }}) + } + {%- endif %} + + {%- if self.gas_checkpoints %} + gas_checkpoint(5) // after theta squeeze + lookup multiplicities + {%- endif %} + + // ---- beta, gamma ---- + // beta and gamma are the permutation/lookup randomizers. They are + // squeezed after lookup multiplicities and before permutation + // product commitments, matching the native verifier schedule. + buf_len := squeeze_to(buf_len, BETA_MPTR) + buf_len := squeeze_to(buf_len, GAMMA_MPTR) + + {%- if num_permutation_zs != 0 %} + // ---- permutation Z products ---- + // Permutation product commitments are used by the permutation + // identities in the quotient numerator and later by PCS openings. + let perm_z_walk := PERM_Z_COMMS_MPTR_BASE + for { let end := add(proof_cptr, {{ codegen_layout.proof.permutation_products.byte_len|hex() }}) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(perm_z_walk, proof_cptr, {{ template_constants.g1_bytes|hex() }}) + {%- if self.trace %} + trace_point(proof_commit_trace_id, perm_z_walk) + proof_commit_trace_id := add(proof_commit_trace_id, 1) + {%- endif %} + perm_z_walk := add(perm_z_walk, {{ template_constants.g1_bytes|hex() }}) + proof_cptr := add(proof_cptr, {{ template_constants.g1_bytes|hex() }}) + } + {%- endif %} + + {%- if self.gas_checkpoints %} + gas_checkpoint(6) // after beta/gamma + permutation Z products + {%- endif %} + + {%- if lookup_h_plus_acc != 0 %} + // ---- lookup helpers + accumulators (per-lookup) ---- + // Each lookup contributes zero or more helper commitments followed + // by its lookup accumulator Z commitment. The generated layout keeps + // helper commitments and accumulator commitments in separate memory + // regions because the quotient/PCS schedules address them + // differently. + let lookup_helper_walk := LOOKUP_HELPER_COMMS_MPTR_BASE + let lookup_z_walk := LOOKUP_Z_COMMS_MPTR_BASE + {%- for lookup in codegen_layout.proof.lookups %} + // lookup {{ loop.index0 }}: {{ lookup.helpers.item_count }} helper(s) + 1 acc + // Helper commitments for lookup {{ loop.index0 }}. + for { let end := add(proof_cptr, {{ lookup.helpers.byte_len|hex() }}) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(lookup_helper_walk, proof_cptr, {{ template_constants.g1_bytes|hex() }}) + {%- if self.trace %} + trace_point(proof_commit_trace_id, lookup_helper_walk) + proof_commit_trace_id := add(proof_commit_trace_id, 1) + {%- endif %} + lookup_helper_walk := add(lookup_helper_walk, {{ template_constants.g1_bytes|hex() }}) + proof_cptr := add(proof_cptr, {{ template_constants.g1_bytes|hex() }}) + } + // Accumulator commitment for lookup {{ loop.index0 }}. This is + // always one G1 when the lookup section is present. + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(lookup_z_walk, proof_cptr, {{ template_constants.g1_bytes|hex() }}) + {%- if self.trace %} + trace_point(proof_commit_trace_id, lookup_z_walk) + proof_commit_trace_id := add(proof_commit_trace_id, 1) + {%- endif %} + lookup_z_walk := add(lookup_z_walk, {{ template_constants.g1_bytes|hex() }}) + proof_cptr := add(proof_cptr, {{ template_constants.g1_bytes|hex() }}) + {%- endfor %} + {%- endif %} + + {%- if self.gas_checkpoints %} + gas_checkpoint(7) // after lookup helpers + Z accumulators + {%- endif %} + + // ---- trash_challenge ---- + // Midnight squeezes this challenge unconditionally, even when the + // circuit has no trash arguments. + // Keeping this squeeze unconditional preserves transcript + // compatibility across circuits with and without trash columns. + buf_len := squeeze_to(buf_len, TRASH_CHALLENGE_MPTR) + {%- if num_trashcans != 0 %} + // ---- trashcans ---- + // Trashcan commitments are optional, but when present they are + // absorbed before y so the quotient batching challenge binds them. + let trashcan_walk := TRASHCAN_COMMS_MPTR_BASE + for { let end := add(proof_cptr, {{ codegen_layout.proof.trash.byte_len|hex() }}) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(trashcan_walk, proof_cptr, {{ template_constants.g1_bytes|hex() }}) + {%- if self.trace %} + trace_point(proof_commit_trace_id, trashcan_walk) + proof_commit_trace_id := add(proof_commit_trace_id, 1) + {%- endif %} + trashcan_walk := add(trashcan_walk, {{ template_constants.g1_bytes|hex() }}) + proof_cptr := add(proof_cptr, {{ template_constants.g1_bytes|hex() }}) + } + {%- endif %} + + {%- if self.gas_checkpoints %} + gas_checkpoint(8) // after trash_challenge + trashcans + {%- endif %} + + // ---- y ---- + // y batches all quotient identities. Quotient commitments are read + // only after y is sampled, matching the Rust verifier flow. + buf_len := squeeze_to(buf_len, Y_MPTR) + + // ---- quotient commitment(s) ---- + // Each uncompressed quotient commitment is calldatacopied directly to + // QUOTIENT_LIMB_COMMS_MPTR_BASE; the Horner fold below reads + // them back from memory. common_uncompressed_g1 absorbs the + // 128-byte calldata form into the transcript verbatim. + // + // Multi-limb quotient mode reads several Q_i commitments; single-H + // mode renders this loop with one limb. + let quotient_walk := QUOTIENT_LIMB_COMMS_MPTR_BASE + for { let end := add(proof_cptr, {{ codegen_layout.proof.quotient_limbs.byte_len|hex() }}) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(quotient_walk, proof_cptr, {{ template_constants.g1_bytes|hex() }}) + {%- if self.trace %} + trace_point(proof_commit_trace_id, quotient_walk) + proof_commit_trace_id := add(proof_commit_trace_id, 1) + {%- endif %} + quotient_walk := add(quotient_walk, {{ template_constants.g1_bytes|hex() }}) + proof_cptr := add(proof_cptr, {{ template_constants.g1_bytes|hex() }}) + } + + {%- if self.gas_checkpoints %} + gas_checkpoint(9) // after y squeeze + quotient-limb reads + {%- endif %} + + // ---- x ---- + // x is the main evaluation point. Values read after this point are + // alleged polynomial evaluations at x or derived PCS openings. + buf_len := squeeze_to(buf_len, X_MPTR) + + // ---- evaluations ---- + // Optimisation H3: the off-chain Solidity proof shim rewrites + // proof scalars into BE calldata words. Spill each decoded eval + // into REVERSED_EVALS_MPTR in the same iteration we range-check + // it, so downstream references can use cheap mload. + // + // The Rust verifier conceptually reads evaluations in query order. + // The lowering plan arranges REVERSED_EVALS_MPTR in the order used + // by the quotient VM/direct evaluator, hence the generated name. + { + let eval_buf := REVERSED_EVALS_MPTR + for { let end := add(proof_cptr, {{ codegen_layout.proof.evals.byte_len|hex() }}) } + lt(proof_cptr, end) + {} { + let eval := calldataload(proof_cptr) + // Proof evaluation scalars must be canonical Fr elements + // before they are absorbed or made available to quotient + // reconstruction. + if iszero(lt(eval, r)) { revert(0, 0) } + // Spill for quotient numerator and PCS codegen. + mstore(eval_buf, eval) + eval_buf := add(eval_buf, {{ template_constants.word_bytes|hex() }}) + // Absorb the exact BE field word used by the native + // Keccak transcript. + buf_len := common_word(buf_len, eval) + {%- if self.trace %} + trace_u256(proof_eval_trace_id, eval) + proof_eval_trace_id := add(proof_eval_trace_id, 1) + {%- endif %} + proof_cptr := add(proof_cptr, {{ template_constants.word_bytes|hex() }}) + } + } + + // ---- x1, x2 ---- + // x1 and x2 batch the KZG multi-opening reduction. They are + // squeezed after all polynomial evaluations are absorbed. + buf_len := squeeze_to(buf_len, X1_MPTR) + buf_len := squeeze_to(buf_len, X2_MPTR) + + // ---- f_com (1 uncompressed G1) ---- + // f_com is the commitment to the batched polynomial used by the PCS + // multi-open protocol. It is both transcript material and later + // pairing/MSM input. + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(F_COM_MPTR, proof_cptr, {{ template_constants.g1_bytes|hex() }}) + {%- if self.trace %} + trace_point(proof_commit_trace_id, F_COM_MPTR) + proof_commit_trace_id := add(proof_commit_trace_id, 1) + {%- endif %} + proof_cptr := add(proof_cptr, {{ template_constants.g1_bytes|hex() }}) + + // ---- x3 ---- + // x3 is the PCS evaluation point for f_com. + buf_len := squeeze_to(buf_len, X3_MPTR) + {%- if truncated_challenges %} + // truncated-challenges mirrors midnight-proofs + // proofs/src/poly/kzg/mod.rs: + // - x3 is the f_com evaluation point and is truncated + // immediately after squeeze. + // - x1 and x4 remain full squeezed Fr words, but later PCS + // batching stores truncate(x1^i) and truncate(x4^i) while + // keeping the internal power accumulators full precision. + // This direct x3 mask is therefore one part of the PCS truncation + // rule, not the only truncated value used by the verifier. + mstore(X3_MPTR, and(mload(X3_MPTR), 0xffffffffffffffffffffffffffffffff)) + {%- endif %} + + // ---- q_evals (one Fq per point set) ---- + // q_evals are not spilled into REVERSED_EVALS_MPTR because the PCS + // emitter reads them as a contiguous calldata range from the saved + // Q_EVAL_CPTR_MPTR cursor. + // + // Each q_eval is the claimed evaluation for one prepared point set + // in the KZG multi-open reduction. They are still transcript + // material and must be range-checked as Fr scalars. + mstore(Q_EVAL_CPTR_MPTR, proof_cptr) + for { let end := add(proof_cptr, {{ codegen_layout.proof.q_evals.byte_len|hex() }}) } + lt(proof_cptr, end) + {} { + let eval := calldataload(proof_cptr) + // Canonical Fr check before transcript absorption. + if iszero(lt(eval, r)) { revert(0, 0) } + buf_len := common_word(buf_len, eval) + {%- if self.trace %} + trace_u256(proof_eval_trace_id, eval) + proof_eval_trace_id := add(proof_eval_trace_id, 1) + {%- endif %} + proof_cptr := add(proof_cptr, {{ template_constants.word_bytes|hex() }}) + } + + // ---- x4 ---- + // x4 is the final PCS batching challenge, sampled after q_evals + // and before the opening proof point pi. + buf_len := squeeze_to(buf_len, X4_MPTR) + + // ---- pi (1 uncompressed G1) ---- + // pi is the KZG opening proof commitment. It is the last proof + // object absorbed into the transcript and later becomes one side of + // the final pairing check. + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(PI_MPTR, proof_cptr, {{ template_constants.g1_bytes|hex() }}) + {%- if self.trace %} + trace_point(proof_commit_trace_id, PI_MPTR) + proof_commit_trace_id := add(proof_commit_trace_id, 1) + {%- endif %} + proof_cptr := add(proof_cptr, {{ template_constants.g1_bytes|hex() }}) + + // The hand-rolled proof parser must consume exactly the ABI + // `proof` bytes before the `instances` length word. This is + // redundant with the generated proof length today, but makes + // future proof-layout drift fail closed. + // + // NUM_INSTANCE_CPTR is the calldata word immediately after the + // dynamic proof bytes payload. If proof_cptr lands anywhere else, + // some section was under-read or over-read. + if iszero(eq(proof_cptr, NUM_INSTANCE_CPTR)) { revert(0, 0) } + + // `success` carries deferred canonicality failures from public + // instance reads. G1/proof scalar helpers revert immediately. + if iszero(success) { revert(0, 0) } + + {%- if self.gas_checkpoints %} + gas_checkpoint(10) // after evaluations + x1/x2 + f_com + x3 + q_evals + x4 + pi (transcript done) + {%- endif %} diff --git a/proofs/solidity-verifier/templates/partials/verifier/VkLoading.yul b/proofs/solidity-verifier/templates/partials/verifier/VkLoading.yul new file mode 100644 index 000000000..3615efae0 --- /dev/null +++ b/proofs/solidity-verifier/templates/partials/verifier/VkLoading.yul @@ -0,0 +1,126 @@ + {%- if self.gas_checkpoints %} + gas_checkpoint(1) // entry: before VK loading + {%- endif %} + + // =============================================================== + // VK loading: either bake in the embedded VK bytes or fetch + // them from the linked AUTHORIZED_VK contract. + // + // This is the first verifier phase after helper definitions. Its + // job is to make the generated VK payload available at VK_MPTR in + // one canonical memory layout, regardless of whether this render + // embeds the VK directly or links a separate Halo2VerifyingKey + // contract. + // + // Later template partials treat VK_MPTR as already populated with: + // - header words: vk_digest, domain data, accumulator metadata; + // - BLS12-381 base points used by the final pairing; + // - compact quotient VM constants/program bytes, when enabled; + // - fixed and permutation commitments in 4-word G1 slots. + // =============================================================== + { + {%- match self.embedded_vk %} + {%- when Some with (embedded_vk) %} + // Embedded VK render: materialize the generated payload + // directly into the same VK_MPTR memory layout that the + // external-VK path obtains via extcodecopy. + // + // Embedded mode increases verifier bytecode size but avoids an + // external VK deployment and proof-time extcodecopy. The + // generated constants below are still organized as the same VK + // payload words used by the separate contract. + // The trailing inline names identify generated payload slots. + {%- for (name, chunk) in embedded_vk.constants %} + // Header/base/quotient-payload word {{ loop.index0 }}. + mstore({{ vk_mptr + loop.index0 }}, {{ chunk|hex_padded(64) }}) // {{ name }} + {%- endfor %} + {%- for (x_hi, x_lo, y_hi, y_lo) in embedded_vk.fixed_comms %} + {%- let offset = embedded_vk.constants.len() %} + // Fixed-column commitment {{ loop.index0 }}. + // Stored as EIP-2537 padded uncompressed G1: + // x_hi, x_lo, y_hi, y_lo. + mstore({{ vk_mptr + offset + 4 * loop.index0 }}, {{ x_hi|hex_padded(64) }}) + mstore({{ vk_mptr + offset + 4 * loop.index0 + 1 }}, {{ x_lo|hex_padded(64) }}) + mstore({{ vk_mptr + offset + 4 * loop.index0 + 2 }}, {{ y_hi|hex_padded(64) }}) + mstore({{ vk_mptr + offset + 4 * loop.index0 + 3 }}, {{ y_lo|hex_padded(64) }}) + {%- endfor %} + {%- for (x_hi, x_lo, y_hi, y_lo) in embedded_vk.permutation_comms %} + {%- let offset = embedded_vk.constants.len() + 4 * embedded_vk.fixed_comms.len() %} + // Permutation commitment {{ loop.index0 }}. + // These follow fixed commitments contiguously in the generated + // VK payload, also as 4-word EIP-2537 G1 slots. + mstore({{ vk_mptr + offset + 4 * loop.index0 }}, {{ x_hi|hex_padded(64) }}) + mstore({{ vk_mptr + offset + 4 * loop.index0 + 1 }}, {{ x_lo|hex_padded(64) }}) + mstore({{ vk_mptr + offset + 4 * loop.index0 + 2 }}, {{ y_hi|hex_padded(64) }}) + mstore({{ vk_mptr + offset + 4 * loop.index0 + 3 }}, {{ y_lo|hex_padded(64) }}) + {%- endfor %} + {%- when None %} + // Re-check the pinned VK dependency on every proof. The + // constructor check catches normal deployment mistakes, while + // this fresh check hardens forks or same-transaction edge + // cases where code at the authorized address could differ + // from the runtime originally pinned by this verifier. + // + // EXPECTED_VK_LENGTH includes the leading INVALID byte in the + // Halo2VerifyingKey runtime. EXPECTED_VK_CODEHASH_WORD is the + // full runtime hash, not only the payload hash. + if iszero(and( + eq(extcodesize(vk), EXPECTED_VK_LENGTH), + eq(extcodehash(vk), EXPECTED_VK_CODEHASH_WORD) + )) { revert(0, 0) } + // Runtime byte 0 is INVALID so direct calls cannot execute the + // payload. Copy from byte 1 into VK_MPTR to reconstruct the + // exact payload layout used by the embedded branch. + extcodecopy(vk, VK_MPTR, 0x01, EXPECTED_VK_PAYLOAD_LENGTH) + {%- endmatch %} + + // This verifier is pinned to one generated VK, so schema + // values such as instance count and accumulator layout are + // rendered as constants instead of reread from the VK header. + // + // The checks below validate the dynamic ABI envelope before the + // transcript parser starts walking raw calldata: + // - proof bytes length equals the generated proof layout; + // - instance array length equals the generated public input + // count; + // - total calldata length has no missing or trailing words. + // + // `success` is folded through `and` for consistency with later + // sections, then immediately enforced at the end of this block. + // A failure here means the verifier is not looking at the proof + // shape it was generated to parse. + success := and(success, eq({{ proof_len|hex() }}, calldataload(PROOF_LEN_CPTR))) + success := and(success, eq({{ num_instances }}, calldataload(NUM_INSTANCE_CPTR))) + // Calldata must contain exactly the ABI selector, proof bytes, + // instance-array length, and generated number of instance + // words. Any trailing bytes fail closed. + success := and( + success, + eq(calldatasize(), add(INSTANCE_CPTR, {{ (num_instances * 32)|hex() }})) + ) + // Stop before any transcript absorption if the ABI/proof shape + // is not exactly the generated one. + if iszero(success) { revert(0, 0) } + } + + {%- if self.expected_has_accumulator %} + // Fail malformed accumulator public inputs before transcript, + // quotient, PCS, and final pairing work. The late accumulator block + // only batches these already-validated G1 outputs into the final + // pairing equation. + // + // Accumulator validation decodes shifted public-input limbs into + // EIP-2537 G1 slots, checks canonical encodings, and routes points + // through G1MSM for curve/subgroup validation. Doing it here means + // invalid accumulator public inputs cannot influence transcript + // challenge derivation or waste gas in later quotient/PCS work. + // validate_public_accumulator returns a boolean to share the same + // success-plumbing style as other helper calls; this boundary is + // where the verifier converts failure to a revert. + success := validate_public_accumulator(success, r) + if iszero(success) { revert(0, 0) } + {%- endif %} + + {%- if self.gas_checkpoints %} + gas_checkpoint(2) // after VK loading + accumulator public-input precheck + {%- endif %} diff --git a/proofs/solidity-verifier/tests/fcom_decompress.rs b/proofs/solidity-verifier/tests/fcom_decompress.rs new file mode 100644 index 000000000..18159cb73 --- /dev/null +++ b/proofs/solidity-verifier/tests/fcom_decompress.rs @@ -0,0 +1,57 @@ +//! Quick sanity check: decompress various proof G1s using midnight-curves' +//! blst-backed G1 decoder and check what we get vs. what the Yul does. + +#![cfg(feature = "evm")] + +use group::{prime::PrimeCurveAffine, GroupEncoding}; +use midnight_curves::G1Affine; + +#[test] +#[ignore = "diagnostic-only compressed point probe; captured bytes are not part of CI acceptance"] +fn fcom_decompress() { + let bytes_hex = "e910f0d3fc7d02235d9c7953d44923684ef8a25ba83e8c80e345f7c2b4827257cabfb6830aa5aefacb690f81404d0b2d"; + let bytes: [u8; 48] = hex::decode(bytes_hex).unwrap().try_into().unwrap(); + let mut compressed = ::Repr::default(); + compressed.as_mut().copy_from_slice(&bytes); + let opt = G1Affine::from_bytes(&compressed); + let success = bool::from(opt.is_some()); + eprintln!("from_bytes ok: {}", success); + let Some(p) = Option::::from(opt) else { + eprintln!("captured f_com bytes do not decode"); + return; + }; + eprintln!("is_identity: {}", bool::from(p.is_identity())); + let bytes2 = p.to_bytes(); + eprintln!("re-serialized hex: {}", hex::encode(bytes2.as_ref())); + eprintln!("matches: {}", bytes2.as_ref() == bytes); +} + +/// Decompress one of the quotient limb compressed bytes captured by the +/// Yul probe and dump the resulting (x, y) for cross-check. +#[test] +#[ignore = "diagnostic-only compressed point probe; captured bytes are not part of CI acceptance"] +fn quotient_limb_decompress() { + // Captured from the Yul `return(0x500, 0xc0)` probe in the previous run. + let compressed_hex = "8c0b3ee2ff547acc7650d83ad9bbe7ccc82f4563026996f058831e9555a9b99ebbfdd23a31fd4d6f71983ba017bb87fc"; + let bytes: [u8; 48] = hex::decode(compressed_hex).unwrap().try_into().unwrap(); + let mut repr = ::Repr::default(); + repr.as_mut().copy_from_slice(&bytes); + let opt = G1Affine::from_bytes(&repr); + let success = bool::from(opt.is_some()); + eprintln!("decompress ok: {}", success); + assert!(success, "quotient limb should decompress"); + let p = opt.unwrap(); + eprintln!("identity: {}", bool::from(p.is_identity())); + // Print x, y as 48-byte big-endian hex. + let x_bytes = p.x().to_bytes_be(); + let y_bytes = p.y().to_bytes_be(); + eprintln!("blst x = {}", hex::encode(x_bytes)); + eprintln!("blst y = {}", hex::encode(y_bytes)); + // Yul-produced (from the probe output we just got). + let yul_x = "0c0b3ee2ff547acc7650d83ad9bbe7ccc82f4563026996f058831e9555a9b99ebbfdd23a31fd4d6f71983ba017bb87fc"; + let yul_y = "068a5df3c9c3cbc6ecefea3b311bd79ead7eff0cd580007152221b72407b8b2218934f928ef73ff3c22f6f78b2b444c1"; + eprintln!("yul x = {yul_x}"); + eprintln!("yul y = {yul_y}"); + eprintln!("x match: {}", hex::encode(x_bytes) == yul_x); + eprintln!("y match: {}", hex::encode(y_bytes) == yul_y); +} diff --git a/proofs/solidity-verifier/tests/ivc_keccak_solidity.rs b/proofs/solidity-verifier/tests/ivc_keccak_solidity.rs new file mode 100644 index 000000000..eea83735a --- /dev/null +++ b/proofs/solidity-verifier/tests/ivc_keccak_solidity.rs @@ -0,0 +1,1794 @@ +//! End-to-end on-chain verification of a Keccak-transcript decider proof for +//! a two-leaf IVC Poseidon hash-chain tree. +//! +//! Pipeline: +//! 1. Build the IVC circuit (k = 19, PoseidonChain transition). +//! 2. Produce two independent one-step IVC proofs under the Poseidon +//! transcript; these are the leaves of the tree. +//! 3. Build a final decider circuit (k = 20) that verifies both IVC leaves, +//! accumulates their final proof accumulators, and fully collapses the +//! result over the fixed IVC VK bases. +//! 4. Prove that decider circuit under Keccak-256, render `Halo2Verifier.sol` +//! + `Halo2VerifyingKey.sol` against the decider VK with +//! `truncated-challenges`. +//! 5. Compile the Solidity, deploy on Prague-spec revm (EIP-2537 precompiles +//! routed through blst), repack the proof off-chain via +//! `SolidityGenerator::encode_calldata`, encode calldata, call +//! `verifyProof`. +//! 6. Assert success and dump gas. +//! +//! Required features: `evm`, `truncated-challenges`. The bench runner enables +//! `in-circuit-fewer-point-sets`; enable `outer-single-h-commitment` explicitly +//! to benchmark the single-H final proof layout. +//! Midnight crates are pulled from the immutable Midfall revision pinned in +//! `Cargo.toml`; `SRS_DIR` still needs to point at local SRS assets. +//! Run: +//! +//! ```text +//! HALO2_SOLIDITY_RUN_IVC_BENCH=1 \ +//! SRS_DIR=/path/to/midfall/zk_stdlib/examples/assets \ +//! cargo test --release \ +//! --features evm,truncated-challenges,in-circuit-fewer-point-sets \ +//! --test ivc_keccak_solidity \ +//! -- --nocapture +//! ``` +//! +//! Enable the detailed gas benchmark with: +//! +//! ```text +//! HALO2_SOLIDITY_RUN_IVC_BENCH=1 \ +//! SRS_DIR=/path/to/midfall/zk_stdlib/examples/assets \ +//! cargo test --release \ +//! --features evm,truncated-challenges,in-circuit-fewer-point-sets,solidity-gas-checkpoints \ +//! --test ivc_keccak_solidity ivc_final_keccak_solidity_e2e \ +//! -- --nocapture +//! ``` +//! +//! Native Rust/Solidity trace equivalence needs a local Midfall checkout that +//! exposes `midnight_proofs::plonk::solidity_trace`. + +#![cfg(all(feature = "evm", feature = "truncated-challenges",))] + +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, + time::Instant, +}; +#[cfg(feature = "outer-single-h-commitment")] +use std::{fs::File, io::BufReader}; + +use ff::{Field, PrimeField}; +use group::{Group, GroupEncoding}; +use halo2_solidity_verifier::{ + compile_solidity_with_runs, pinned_solc_available, solc_version, AccumulatorEncoding, + CallOutcome, Evm, GeneratorConfig, ProofEvaluationCounts, QuotientIdentitySource, + RenderDiagnostics, RenderOptions, RenderQuotient, RenderVk, SolidityGenerator, + PINNED_SOLC_VERSION, +}; +use midnight_aggregation::ivc::{self, IvcCircuit, IvcContext, IvcIO, IvcState, IvcTransition}; +use midnight_circuits::{ + hash::poseidon::{PoseidonChip, PoseidonState}, + instructions::{hash::HashCPU, *}, + types::{AssignedBit, AssignedNative, InnerValue, Instantiable}, + verifier::{self, Accumulator, AssignedAccumulator, BlstrsEmulation, Msm, SelfEmulation}, +}; +use midnight_proofs::{ + circuit::{Layouter, Value}, + plonk::{self, ConstraintSystem, Error}, + poly::{ + kzg::{ + params::{ParamsKZG, ParamsVerifierKZG}, + scoped_fewer_point_sets, KZGCommitmentScheme, + }, + EvaluationDomain, + }, + transcript::{CircuitTranscript, Transcript}, +}; +#[cfg(feature = "outer-single-h-commitment")] +use midnight_proofs::{poly::commitment::Params as _, utils::SerdeFormat}; +use midnight_zk_stdlib::{ + cs_degree, + utils::plonk_api::{load_srs, SrsSource}, + MidnightVK, Relation, ZkStdLib, ZkStdLibArch, +}; +use rand::rngs::OsRng; + +type S = BlstrsEmulation; +type F = ::F; +type C = ::C; +type E = ::Engine; + +const RUN_IVC_BENCH_ENV: &str = "HALO2_SOLIDITY_RUN_IVC_BENCH"; +const WRITE_IVC_LEAF_BUNDLE_ENV: &str = "HALO2_SOLIDITY_WRITE_IVC_LEAF_BUNDLE"; +const IVC_LEAF_BUNDLE_PATH_ENV: &str = "HALO2_SOLIDITY_IVC_LEAF_BUNDLE"; +const IVC_LEAF_BUNDLE_MAGIC: &[u8] = b"IVC_LEAF_BUNDLE_V1"; +const G1_COMPRESSED_BYTES: usize = 48; + +// --------------------------------------------------------------------------- +// IVC Poseidon hash-chain transition (mirror of +// midfall/aggregation/examples/ivc.rs). +// --------------------------------------------------------------------------- + +const POSEIDON_HASHES_PER_STEP: usize = 1; + +type Chain = PoseidonChain; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct State { + cnt: F, + val: F, +} + +#[derive(Clone, Debug)] +struct AssignedState { + cnt: AssignedNative, + val: AssignedNative, +} + +#[derive(Clone, Debug)] +struct PoseidonChain { + std_lib: ZkStdLib, +} + +impl IvcContext for PoseidonChain { + type Context = (); + + fn new(std_lib: ZkStdLib, _ctx: &()) -> Self { + PoseidonChain { std_lib } + } + + fn write_context(_ctx: &(), _writer: &mut W) -> std::io::Result<()> { + Ok(()) + } + + fn read_context(_reader: &mut R) -> std::io::Result<()> { + Ok(()) + } +} + +impl IvcState for PoseidonChain { + type State = State; + type AssignedState = AssignedState; + + fn genesis(_ctx: &()) -> Self::State { + State { + cnt: F::ZERO, + val: F::ZERO, + } + } + + fn is_genesis( + &self, + layouter: &mut impl Layouter, + state: &Self::AssignedState, + ) -> Result, Error> { + let cnt_is_zero = self.std_lib.bls12_381_scalar().is_zero(layouter, &state.cnt)?; + let val_is_zero = self.std_lib.bls12_381_scalar().is_zero(layouter, &state.val)?; + self.std_lib.and(layouter, &[cnt_is_zero, val_is_zero]) + } + + fn decider(_ctx: &Self::Context, _state: &Self::State) -> bool { + true + } +} + +impl IvcIO for PoseidonChain { + fn assign( + &self, + layouter: &mut impl Layouter, + value: Value, + ) -> Result { + let scalar_chip = self.std_lib.bls12_381_scalar(); + Ok(AssignedState { + cnt: scalar_chip.assign(layouter, value.as_ref().map(|s| s.cnt))?, + val: scalar_chip.assign(layouter, value.as_ref().map(|s| s.val))?, + }) + } + + fn constrain_as_public_input( + &self, + layouter: &mut impl Layouter, + state: &AssignedState, + ) -> Result<(), Error> { + let scalar_chip = self.std_lib.bls12_381_scalar(); + scalar_chip.constrain_as_public_input(layouter, &state.cnt)?; + scalar_chip.constrain_as_public_input(layouter, &state.val) + } + + fn as_public_input( + &self, + _layouter: &mut impl Layouter, + state: &AssignedState, + ) -> Result>, Error> { + Ok(vec![state.cnt.clone(), state.val.clone()]) + } + + fn format_public_input(state: &State) -> Vec { + vec![state.cnt, state.val] + } +} + +impl IvcTransition for PoseidonChain { + type Witness = (); + + fn arch() -> ZkStdLibArch { + ZkStdLibArch { + poseidon: true, + nr_pow2range_cols: 4, + ..ZkStdLibArch::default() + } + } + + fn transition(_ctx: &(), state: &Self::State, _witness: Self::Witness) -> Self::State { + let mut val = state.val; + for _ in 0..N { + val = as HashCPU>::hash(&[val]); + } + State { + cnt: state.cnt + F::from(N as u64), + val, + } + } + + fn circuit_transition( + &self, + layouter: &mut impl Layouter, + state: &Self::AssignedState, + _witness: Value, + ) -> Result { + let scalar_chip = self.std_lib.bls12_381_scalar(); + let mut val = state.val.clone(); + for _ in 0..N { + val = self.std_lib.poseidon(layouter, &[val])?; + } + let cnt = scalar_chip.add_constant(layouter, &state.cnt, F::from(N as u64))?; + Ok(AssignedState { cnt, val }) + } +} + +fn fully_collapsed_accumulator( + acc: &Accumulator, + fixed_bases: &BTreeMap, +) -> Accumulator { + let (lhs, rhs) = acc.fully_collapse(fixed_bases); + Accumulator::new( + Msm::from_terms(&[lhs], &[F::ONE]), + Msm::from_terms(&[rhs], &[F::ONE]), + ) +} + +fn constrain_fully_collapsed_accumulator( + std_lib: &ZkStdLib, + layouter: &mut impl Layouter, + acc: AssignedAccumulator, + fixed_bases: &BTreeMap, +) -> Result, Error> { + let (lhs, rhs) = acc.fully_collapse(layouter, std_lib.bls12_381_curve(), fixed_bases)?; + let collapsed_value = acc.value().map(|acc| fully_collapsed_accumulator(&acc, fixed_bases)); + let collapsed = + std_lib + .verifier() + .assign_collapsed_accumulator(layouter, &[], collapsed_value)?; + + let one: AssignedNative = std_lib.assign_fixed(layouter, F::ONE)?; + let expected = [ + std_lib.bls12_381_curve().as_public_input(layouter, &lhs)?, + vec![one.clone()], + std_lib.bls12_381_curve().as_public_input(layouter, &rhs)?, + vec![one], + ] + .concat(); + let actual = std_lib.verifier().as_public_input(layouter, &collapsed)?; + assert_eq!(actual.len(), expected.len()); + for (actual, expected) in actual.iter().zip(expected.iter()) { + std_lib.assert_equal(layouter, actual, expected)?; + } + + Ok(collapsed) +} + +fn constrain_same_accumulator_public_input( + std_lib: &ZkStdLib, + layouter: &mut impl Layouter, + lhs: &AssignedAccumulator, + rhs: &AssignedAccumulator, +) -> Result<(), Error> { + let lhs = std_lib.verifier().as_public_input(layouter, lhs)?; + let rhs = std_lib.verifier().as_public_input(layouter, rhs)?; + assert_eq!(lhs.len(), rhs.len()); + for (lhs, rhs) in lhs.iter().zip(rhs.iter()) { + std_lib.assert_equal(layouter, lhs, rhs)?; + } + Ok(()) +} + +// --------------------------------------------------------------------------- +// Final two-leaf tree decider. +// --------------------------------------------------------------------------- + +const TREE_LEAVES: usize = 2; + +#[derive(Clone, Debug)] +struct TreeDeciderContext { + ivc_cs: ConstraintSystem, + ivc_domain: EvaluationDomain, + ivc_vk: MidnightVK, + ivc_params_verifier: ParamsVerifierKZG, +} + +impl TreeDeciderContext { + fn ivc_fixed_bases(&self) -> BTreeMap { + verifier::fixed_bases::("ivc_vk", self.ivc_vk.vk()) + } + + fn ivc_fixed_base_names(&self) -> Vec { + self.ivc_fixed_bases().keys().cloned().collect() + } + + fn one_step_outer_acc(&self) -> Accumulator { + Accumulator::::trivial(&self.ivc_fixed_base_names()) + } + + fn leaf_public_input(&self, state: &State) -> Vec { + [ + vec![self.ivc_vk.vk().transcript_repr()], + Chain::format_public_input(state), + AssignedAccumulator::::as_public_input(&self.one_step_outer_acc()), + ] + .concat() + } + + fn leaf_final_acc(&self, state: &State, proof: &[u8]) -> Accumulator { + let leaf_pi = self.leaf_public_input(state); + let mut transcript = CircuitTranscript::>::init_from_bytes(proof); + let dual_msm = + plonk::prepare::, CircuitTranscript>>( + self.ivc_vk.vk(), + &[&[C::identity()]], + &[&[&leaf_pi]], + &mut transcript, + ) + .expect("off-circuit IVC leaf prepare should succeed"); + transcript.assert_empty().expect("IVC leaf transcript should be consumed"); + assert!( + dual_msm.clone().check(&self.ivc_params_verifier), + "invalid IVC leaf proof" + ); + + let proof_acc = Accumulator::from_dual_msm(dual_msm, "ivc_vk", &self.ivc_fixed_bases()); + Accumulator::accumulate(&[proof_acc, self.one_step_outer_acc()]) + } + + fn final_acc(&self, leaves: &[TreeLeafWitness; TREE_LEAVES]) -> Accumulator { + let leaf_accs = leaves + .iter() + .map(|leaf| self.leaf_final_acc(&leaf.state, &leaf.proof)) + .collect::>(); + let acc = Accumulator::accumulate(&leaf_accs); + fully_collapsed_accumulator(&acc, &self.ivc_fixed_bases()) + } +} + +#[derive(Clone, Debug)] +struct TreeDeciderInstance { + leaf_states: [State; TREE_LEAVES], + final_acc: Accumulator, +} + +#[derive(Clone, Debug)] +struct TreeLeafWitness { + state: State, + proof: Vec, +} + +#[derive(Clone, Debug)] +struct TreeDeciderWitness { + leaves: [TreeLeafWitness; TREE_LEAVES], +} + +#[derive(Clone, Debug)] +struct IvcLeafBundle { + leaves: [TreeLeafWitness; TREE_LEAVES], + final_acc: Accumulator, +} + +fn push_scalar_le(out: &mut Vec, scalar: &F) { + out.extend_from_slice(scalar.to_repr().as_ref()); +} + +fn read_exact<'a>(bytes: &'a [u8], cursor: &mut usize, len: usize, label: &str) -> &'a [u8] { + let end = cursor + .checked_add(len) + .unwrap_or_else(|| panic!("leaf bundle cursor overflow while reading {label}")); + assert!( + end <= bytes.len(), + "leaf bundle ended while reading {label}: need {end} bytes, have {}", + bytes.len() + ); + let out = &bytes[*cursor..end]; + *cursor = end; + out +} + +fn read_scalar_le(bytes: &[u8], cursor: &mut usize, label: &str) -> F { + let mut repr = ::Repr::default(); + repr.as_mut().copy_from_slice(read_exact(bytes, cursor, 32, label)); + Option::from(F::from_repr(repr)) + .unwrap_or_else(|| panic!("leaf bundle contains non-canonical scalar for {label}")) +} + +fn push_g1_compressed(out: &mut Vec, point: &C) { + out.extend_from_slice(::to_bytes(point).as_ref()); +} + +fn read_g1_compressed(bytes: &[u8], cursor: &mut usize, label: &str) -> C { + let mut repr = ::Repr::default(); + repr.as_mut() + .copy_from_slice(read_exact(bytes, cursor, G1_COMPRESSED_BYTES, label)); + Option::from(C::from_bytes(&repr)) + .unwrap_or_else(|| panic!("leaf bundle contains malformed compressed G1 for {label}")) +} + +fn write_ivc_leaf_bundle(path: &Path, bundle: &IvcLeafBundle) { + let mut bytes = Vec::new(); + bytes.extend_from_slice(IVC_LEAF_BUNDLE_MAGIC); + bytes.extend_from_slice(&(TREE_LEAVES as u32).to_le_bytes()); + for leaf in &bundle.leaves { + push_scalar_le(&mut bytes, &leaf.state.cnt); + push_scalar_le(&mut bytes, &leaf.state.val); + let proof_len: u32 = leaf.proof.len().try_into().expect("leaf proof length fits u32"); + bytes.extend_from_slice(&proof_len.to_le_bytes()); + bytes.extend_from_slice(&leaf.proof); + } + + let no_fixed_bases = BTreeMap::new(); + let (lhs, rhs) = bundle.final_acc.fully_collapse(&no_fixed_bases); + push_g1_compressed(&mut bytes, &lhs); + push_g1_compressed(&mut bytes, &rhs); + + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).expect("create leaf bundle parent directory"); + } + std::fs::write(path, bytes).expect("write IVC leaf bundle"); +} + +fn read_ivc_leaf_bundle(path: &Path) -> IvcLeafBundle { + let bytes = std::fs::read(path).unwrap_or_else(|err| { + panic!( + "failed to read IVC leaf bundle {}: {err}; run scripts/run_ivc_bench.sh so the multi-limb leaf phase is generated first", + path.display() + ) + }); + let mut cursor = 0usize; + assert_eq!( + read_exact(&bytes, &mut cursor, IVC_LEAF_BUNDLE_MAGIC.len(), "magic"), + IVC_LEAF_BUNDLE_MAGIC, + "invalid IVC leaf bundle magic" + ); + let mut count_bytes = [0u8; 4]; + count_bytes.copy_from_slice(read_exact(&bytes, &mut cursor, 4, "leaf count")); + let leaf_count = u32::from_le_bytes(count_bytes) as usize; + assert_eq!( + leaf_count, TREE_LEAVES, + "IVC leaf bundle has {leaf_count} leaves, expected {TREE_LEAVES}" + ); + + let mut leaves = Vec::with_capacity(TREE_LEAVES); + for idx in 0..TREE_LEAVES { + let cnt = read_scalar_le(&bytes, &mut cursor, "leaf cnt"); + let val = read_scalar_le(&bytes, &mut cursor, "leaf val"); + let mut len_bytes = [0u8; 4]; + len_bytes.copy_from_slice(read_exact(&bytes, &mut cursor, 4, "leaf proof length")); + let proof_len = u32::from_le_bytes(len_bytes) as usize; + let proof = read_exact(&bytes, &mut cursor, proof_len, "leaf proof").to_vec(); + leaves.push(TreeLeafWitness { + state: State { cnt, val }, + proof, + }); + println!("[ivc-keccak-solidity] loaded leaf {idx} from bundle: proof = {proof_len} bytes"); + } + + let lhs = read_g1_compressed(&bytes, &mut cursor, "final accumulator lhs"); + let rhs = read_g1_compressed(&bytes, &mut cursor, "final accumulator rhs"); + assert_eq!( + cursor, + bytes.len(), + "IVC leaf bundle has {} trailing byte(s)", + bytes.len() - cursor + ); + + IvcLeafBundle { + leaves: leaves.try_into().expect("exactly two leaves"), + final_acc: Accumulator::new( + Msm::from_terms(&[lhs], &[F::ONE]), + Msm::from_terms(&[rhs], &[F::ONE]), + ), + } +} + +#[derive(Clone, Debug)] +struct IvcTreeDeciderCircuit { + ctx: TreeDeciderContext, +} + +impl IvcTreeDeciderCircuit { + fn new(ctx: TreeDeciderContext) -> Self { + Self { ctx } + } + + fn arch() -> ZkStdLibArch { + ZkStdLibArch { + bls12_381: true, + poseidon: true, + nr_pow2range_cols: 4, + ..ZkStdLibArch::default() + } + } +} + +impl Relation for IvcTreeDeciderCircuit { + type Instance = TreeDeciderInstance; + type Witness = TreeDeciderWitness; + + fn format_instance(instance: &Self::Instance) -> Result, Error> { + let leaf_states = instance + .leaf_states + .iter() + .flat_map(Chain::format_public_input) + .collect::>(); + Ok([ + leaf_states, + AssignedAccumulator::::as_public_input(&instance.final_acc), + ] + .concat()) + } + + fn circuit( + &self, + std_lib: &ZkStdLib, + layouter: &mut impl Layouter, + instance: Value, + witness: Value, + ) -> Result<(), Error> { + let verifier_gadget = std_lib.verifier(); + let poseidon_chain = Chain::new(std_lib.clone(), &()); + + let mut public_leaf_states = Vec::with_capacity(TREE_LEAVES); + for i in 0..TREE_LEAVES { + let cnt = std_lib.assign( + layouter, + instance.as_ref().map(|instance| instance.leaf_states[i].cnt), + )?; + let val = std_lib.assign( + layouter, + instance.as_ref().map(|instance| instance.leaf_states[i].val), + )?; + public_leaf_states.push(vec![cnt, val]); + } + for public_state in &public_leaf_states { + for value in public_state { + std_lib.constrain_as_public_input(layouter, value)?; + } + } + + let public_final_acc = verifier_gadget.assign_collapsed_accumulator( + layouter, + &[], + instance.as_ref().map(|instance| instance.final_acc.clone()), + )?; + verifier_gadget.constrain_as_public_input(layouter, &public_final_acc)?; + + let assigned_ivc_vk = verifier_gadget.assign_fixed_vk( + layouter, + "ivc_vk", + &self.ctx.ivc_domain, + &self.ctx.ivc_cs, + self.ctx.ivc_vk.vk().transcript_repr(), + )?; + let ivc_vk_pi = verifier_gadget.as_public_input(layouter, &assigned_ivc_vk)?; + let id_point: ::AssignedPoint = + std_lib.bls12_381_curve().assign_fixed(layouter, C::identity())?; + let outer_acc_value = Value::known(self.ctx.one_step_outer_acc()); + let outer_acc = verifier_gadget.assign_collapsed_accumulator( + layouter, + &self.ctx.ivc_fixed_base_names(), + outer_acc_value, + )?; + let outer_acc_pi = verifier_gadget.as_public_input(layouter, &outer_acc)?; + + let mut leaf_accs = Vec::with_capacity(TREE_LEAVES); + for (i, public_state) in public_leaf_states.iter().enumerate() { + let leaf_state = poseidon_chain.assign( + layouter, + witness.as_ref().map(|witness| witness.leaves[i].state), + )?; + let leaf_state_pi = poseidon_chain.as_public_input(layouter, &leaf_state)?; + assert_eq!(public_state.len(), leaf_state_pi.len()); + for (public, witnessed) in public_state.iter().zip(leaf_state_pi.iter()) { + std_lib.assert_equal(layouter, public, witnessed)?; + } + + let leaf_pi = [ivc_vk_pi.clone(), leaf_state_pi, outer_acc_pi.clone()].concat(); + let proof_acc = verifier_gadget.prepare( + layouter, + &assigned_ivc_vk, + std::slice::from_ref(&id_point), + &[&leaf_pi], + witness.as_ref().map(|witness| witness.leaves[i].proof.clone()), + )?; + let leaf_acc = verifier_gadget.accumulate(layouter, &[proof_acc, outer_acc.clone()])?; + leaf_accs.push(leaf_acc); + } + + let final_acc = verifier_gadget.accumulate(layouter, &leaf_accs)?; + let final_acc = constrain_fully_collapsed_accumulator( + std_lib, + layouter, + final_acc, + &self.ctx.ivc_fixed_bases(), + )?; + constrain_same_accumulator_public_input(std_lib, layouter, &final_acc, &public_final_acc) + } + + fn used_chips(&self) -> ZkStdLibArch { + Self::arch() + } + + fn write_relation(&self, _writer: &mut W) -> std::io::Result<()> { + Ok(()) + } + + fn read_relation(_reader: &mut R) -> std::io::Result { + unimplemented!() + } +} + +// --------------------------------------------------------------------------- +// e2e test +// --------------------------------------------------------------------------- + +fn ivc_constraint_system(arch: ZkStdLibArch, k: u32) -> (ConstraintSystem, EvaluationDomain) { + let mut cs = ConstraintSystem::default(); + ZkStdLib::configure(&mut cs, (arch, (k - 1) as u8)); + let domain = EvaluationDomain::new(cs.degree() as u32, k); + (cs, domain) +} + +fn srs_dir() -> String { + std::env::var("SRS_DIR").unwrap_or_else(|_| { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../zk_stdlib/examples/assets") + .to_string_lossy() + .into_owned() + }) +} + +fn has_required_srs_assets(required_ks: &[u32]) -> bool { + let srs_dir = srs_dir(); + let mut ok = true; + + for &k in required_ks { + let path = format!("{srs_dir}/midnight-srs-2p{k}"); + if !std::path::Path::new(&path).is_file() { + println!( + "[ivc-keccak-solidity] missing Midnight SRS: {path}\n\ + [ivc-keccak-solidity] download with: curl -L -o {path} https://srs.midnight.network/midnight-srs-2p{k}" + ); + ok = false; + } + } + + ok +} + +fn ivc_leaf_bundle_path() -> PathBuf { + std::env::var(IVC_LEAF_BUNDLE_PATH_ENV).map(PathBuf::from).unwrap_or_else(|_| { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("target") + .join("ivc-keccak-solidity-dump") + .join("ivc-leaf-bundle.bin") + }) +} + +fn ceil_log2_usize(n: usize) -> u32 { + if n <= 1 { + 0 + } else { + usize::BITS - (n - 1).leading_zeros() + } +} + +fn outer_single_h_extended_srs_k(decider_k: u32, cs_degree: usize) -> u32 { + decider_k + ceil_log2_usize(cs_degree.saturating_sub(1).max(1)) +} + +fn required_srs_ks(ivc_k: u32, decider_k: u32, decider_degree: usize) -> Vec { + let mut required = vec![ivc_k, decider_k]; + if halo2_solidity_verifier::OUTER_SINGLE_H_COMMITMENT_ENABLED { + required.push(outer_single_h_extended_srs_k(decider_k, decider_degree)); + } + required.sort_unstable(); + required.dedup(); + required +} + +#[cfg(feature = "outer-single-h-commitment")] +fn read_midnight_srs(k: u32) -> ParamsKZG { + let srs_path = PathBuf::from(srs_dir()).join(format!("midnight-srs-2p{k}")); + let file = File::open(&srs_path) + .unwrap_or_else(|err| panic!("failed to open Midnight SRS {}: {err}", srs_path.display())); + ParamsKZG::::read_custom(&mut BufReader::new(file), SerdeFormat::RawBytesUnchecked) + .unwrap_or_else(|err| panic!("failed to read Midnight SRS {}: {err}", srs_path.display())) +} + +fn load_decider_srs(decider_k: u32, cs_degree: usize) -> ParamsKZG { + if halo2_solidity_verifier::OUTER_SINGLE_H_COMMITMENT_ENABLED { + #[cfg(feature = "outer-single-h-commitment")] + { + let extended_k = outer_single_h_extended_srs_k(decider_k, cs_degree); + let base = read_midnight_srs(decider_k); + let extended = read_midnight_srs(extended_k); + let combined = base.with_extended_monomial(extended); + let required_monomials = ((1usize << decider_k) - 1) + .checked_mul(cs_degree.saturating_sub(1)) + .expect("single-H quotient SRS requirement fits usize"); + assert!( + combined.g_monomial_size() >= required_monomials, + "single-H decider SRS has {} monomial elements, need at least {required_monomials}", + combined.g_monomial_size() + ); + println!( + "[ivc-keccak-solidity] outer single-H SRS: base k={decider_k}, extended monomial k={extended_k}, monomial elements = {}", + combined.g_monomial_size() + ); + combined + } + #[cfg(not(feature = "outer-single-h-commitment"))] + unreachable!("outer single-H flag can only be true when the feature is enabled") + } else { + load_srs(SrsSource::Midnight, decider_k, cs_degree) + } +} + +fn proof_evaluation_count_summary(counts: &ProofEvaluationCounts) -> String { + format!( + "proof eval scalars total: {} (main: {}, dummy PCS: {})\n\ + instances: {} proof evals from committed instance queries, {} public-input evals computed locally ({} total identity inputs)\n\ + advice evals: {}\n\ + fixed evals: {} proof evals ({} simple-selector fixed columns omitted from proof)\n\ + permutation evals: {} total ({} common/sigma, {} product/Z across {} sets)\n\ + lookup evals: {} total ({} multiplicity, {} helper, {} accumulator z/z_next)\n\ + trash evals: {}\n", + counts.proof_total(), + counts.proof_main_total(), + counts.dummy, + counts.committed_instance, + counts.computed_instance, + counts.instance_total_for_identities(), + counts.advice, + counts.fixed, + counts.simple_selector_fixed, + counts.permutation_total(), + counts.permutation_common, + counts.permutation_product, + counts.permutation_sets, + counts.lookup_total(), + counts.lookup_multiplicity, + counts.lookup_helper, + counts.lookup_accumulator, + counts.trash + ) +} + +fn print_proof_evaluation_counts(counts: &ProofEvaluationCounts) { + println!("\n=== IVC Keccak Solidity proof evaluation counts ==="); + for line in proof_evaluation_count_summary(counts).lines() { + println!("[ivc-keccak-solidity][evals] {line}"); + } +} + +fn assert_ivc_aggregation_quotient_manifest(generator: &SolidityGenerator<'_>) { + let manifest = generator.quotient_identity_manifest(); + let normal_gate_names = manifest + .entries + .iter() + .filter_map(|entry| match &entry.source { + QuotientIdentitySource::Gate { gate_name, .. } => Some(gate_name.as_str()), + _ => None, + }) + .collect::>(); + let trash_names = manifest + .entries + .iter() + .filter_map(|entry| match &entry.source { + QuotientIdentitySource::Trash { trash_name, .. } => Some(trash_name.as_str()), + _ => None, + }) + .collect::>(); + let configured_names = normal_gate_names + .iter() + .chain(trash_names.iter()) + .copied() + .collect::>(); + + for expected in [ + "arith_gate", + "12_minus_34", + "parallel_add_gate", + "Foreign-field multiplication", + "Foreign-field normalization", + "Foreign-field EC is_on_curve", + "Foreign-field EC lambda slope", + "Foreign-field EC assert_tangent", + "Foreign-field EC assert_lambda_squared", + "full_round_gate", + "partial_round_gate", + ] { + assert!( + configured_names.contains(expected), + "IVC aggregation manifest should include configured gate {expected}; got normal={normal_gate_names:?}, trash={trash_names:?}" + ); + } + assert_eq!(configured_names.len(), 11); + assert!( + !normal_gate_names.contains("partial_round_gate"), + "partial_round_gate should be represented by trash, not normal gate identities" + ); + assert!( + trash_names.iter().any(|name| name.contains("partial_round_gate")), + "IVC aggregation trash manifest should name partial_round_gate; got {trash_names:?}" + ); + + let lookup_argument_count = manifest + .entries + .iter() + .filter_map(|entry| match &entry.source { + QuotientIdentitySource::Lookup { lookup_index, .. } => Some(*lookup_index), + _ => None, + }) + .collect::>() + .len(); + assert_eq!(lookup_argument_count, 2); + assert_eq!(manifest.lookup_identities, 6); + assert_eq!(manifest.trash_identities, 1); +} + +fn setup_and_prove_ivc_leaves( + ivc_srs: &ParamsKZG, + ivc_k: u32, +) -> ([TreeLeafWitness; TREE_LEAVES], ivc::IvcVerifier) { + let start = Instant::now(); + let (leaf_prover, verifier) = ivc::setup::(ivc_srs.clone(), ivc_k, ()); + println!( + "[ivc-keccak-solidity] IVC setup completed in {:.2?}", + start.elapsed() + ); + + let mut leaf_witnesses = Vec::with_capacity(TREE_LEAVES); + for i in 0..TREE_LEAVES { + let mut prover = leaf_prover.clone(); + let t0 = Instant::now(); + let p = prover.prove_step(()).unwrap(); + let dt = t0.elapsed(); + let inst = prover.instance(); + let t0 = Instant::now(); + verifier.verify::(&(), &inst, &p).unwrap(); + println!( + "[ivc-keccak-solidity] Leaf {i} IVC Poseidon chain: prove {dt:.2?}, verify {:.2?}, state = {:?}", + t0.elapsed(), + inst.state() + ); + leaf_witnesses.push(TreeLeafWitness { + state: *inst.state(), + proof: p, + }); + } + + ( + leaf_witnesses.try_into().expect("exactly two tree leaves"), + verifier, + ) +} + +fn write_leaf_bundle_mode(ivc_k: u32, path: &Path) { + if halo2_solidity_verifier::OUTER_SINGLE_H_COMMITMENT_ENABLED { + panic!( + "IVC leaf bundles must be generated without outer-single-h-commitment so leaf proofs stay multi-limb" + ); + } + if !has_required_srs_assets(&[ivc_k]) { + println!("[ivc-keccak-solidity] required leaf SRS assets missing; skipping"); + return; + } + + let ivc_srs = load_srs(SrsSource::Midnight, ivc_k, IvcCircuit::::cs_degree()); + let (leaf_witnesses, verifier) = setup_and_prove_ivc_leaves(&ivc_srs, ivc_k); + let (ivc_cs, ivc_domain) = ivc_constraint_system(IvcCircuit::::arch(), ivc_k); + let decider_ctx = TreeDeciderContext { + ivc_cs, + ivc_domain, + ivc_vk: verifier.vk().clone(), + ivc_params_verifier: ivc_srs.verifier_params(), + }; + let final_acc = decider_ctx.final_acc(&leaf_witnesses); + let no_fixed_bases = BTreeMap::new(); + assert!( + final_acc.check(&ivc_srs.verifier_params(), &no_fixed_bases), + "multi-limb leaf bundle accumulator must satisfy the pairing invariant" + ); + + write_ivc_leaf_bundle( + path, + &IvcLeafBundle { + leaves: leaf_witnesses, + final_acc, + }, + ); + println!( + "[ivc-keccak-solidity] wrote multi-limb IVC leaf bundle to {}", + path.display() + ); +} + +#[test] +fn ivc_final_keccak_solidity_e2e() { + const IVC_K: u32 = 19; + const DECIDER_K: u32 = 20; + const SOLC_OPTIMIZE_RUNS: u32 = 1; + const EIP170_MAX_RUNTIME_SIZE: usize = 0x6000; + + if !env_flag_enabled(RUN_IVC_BENCH_ENV) { + println!("[ivc-keccak-solidity] set {RUN_IVC_BENCH_ENV}=1 to run the full bench"); + return; + } + + let leaf_bundle_path = ivc_leaf_bundle_path(); + if env_flag_enabled(WRITE_IVC_LEAF_BUNDLE_ENV) { + write_leaf_bundle_mode(IVC_K, &leaf_bundle_path); + return; + } + + let decider_degree = cs_degree(IvcTreeDeciderCircuit::arch()); + + // Bail out cleanly when the pinned solc isn't available. + if !pinned_solc_available() { + println!("[ivc-keccak-solidity] pinned solc not available; skipping"); + return; + } + let required_srs = required_srs_ks(IVC_K, DECIDER_K, decider_degree); + if !has_required_srs_assets(&required_srs) { + println!("[ivc-keccak-solidity] required SRS assets missing; skipping"); + return; + } + + // ---------------------------------------------------------- + // Two independent one-step Poseidon-chain IVC leaves + // (Midnight SRS at k = 19). + // ---------------------------------------------------------- + let ivc_srs = load_srs(SrsSource::Midnight, IVC_K, IvcCircuit::::cs_degree()); + let outer_single_h = halo2_solidity_verifier::OUTER_SINGLE_H_COMMITMENT_ENABLED; + let (leaf_witnesses, verifier, bundled_final_acc) = if outer_single_h { + let start = Instant::now(); + let (_leaf_prover, verifier) = ivc::setup::(ivc_srs.clone(), IVC_K, ()); + println!( + "[ivc-keccak-solidity] IVC setup completed in {:.2?}", + start.elapsed() + ); + let bundle = read_ivc_leaf_bundle(&leaf_bundle_path); + println!( + "[ivc-keccak-solidity] using multi-limb leaf bundle from {}", + leaf_bundle_path.display() + ); + (bundle.leaves, verifier, Some(bundle.final_acc)) + } else { + let (leaf_witnesses, verifier) = setup_and_prove_ivc_leaves(&ivc_srs, IVC_K); + (leaf_witnesses, verifier, None) + }; + + // ---------------------------------------------------------- + // Final tree decider proof under Keccak. + // ---------------------------------------------------------- + let (ivc_cs, ivc_domain) = ivc_constraint_system(IvcCircuit::::arch(), IVC_K); + let decider_ctx = TreeDeciderContext { + ivc_cs, + ivc_domain, + ivc_vk: verifier.vk().clone(), + ivc_params_verifier: ivc_srs.verifier_params(), + }; + let decider_relation = IvcTreeDeciderCircuit::new(decider_ctx.clone()); + let final_acc = bundled_final_acc.unwrap_or_else(|| decider_ctx.final_acc(&leaf_witnesses)); + let decider_instance = TreeDeciderInstance { + leaf_states: std::array::from_fn(|i| leaf_witnesses[i].state), + final_acc, + }; + let no_fixed_bases = BTreeMap::new(); + assert!( + decider_instance.final_acc.check(&ivc_srs.verifier_params(), &no_fixed_bases), + "fully-collapsed carried IVC accumulator must satisfy the pairing invariant" + ); + println!("[ivc-keccak-solidity] collapsed carried IVC accumulator: OK"); + let decider_witness = TreeDeciderWitness { + leaves: leaf_witnesses, + }; + + let decider_srs = load_decider_srs(DECIDER_K, decider_degree); + let start = Instant::now(); + let decider_vk = midnight_zk_stdlib::setup_vk(&decider_srs, &decider_relation); + let decider_pk = midnight_zk_stdlib::setup_pk(&decider_relation, &decider_vk); + println!( + "[ivc-keccak-solidity] tree decider setup completed in {:.2?}", + start.elapsed() + ); + let outer_fewer_point_sets = halo2_solidity_verifier::OUTER_FEWER_POINT_SETS_ENABLED; + if outer_single_h { + println!( + "[ivc-keccak-solidity] outer proof single-H commitment: enabled (one quotient commitment expected)" + ); + } else { + println!( + "[ivc-keccak-solidity] outer proof single-H commitment: disabled (multi-limb quotient commitments)" + ); + } + if outer_fewer_point_sets { + println!( + "[ivc-keccak-solidity] outer proof fewer-point-sets: enabled (dummy query evals expected)" + ); + } else { + println!( + "[ivc-keccak-solidity] outer proof fewer-point-sets: disabled (in-circuit verifier still uses fewer-point-sets)" + ); + } + + let t0 = Instant::now(); + let final_proof = { + let _outer_proof_layout = scoped_fewer_point_sets(outer_fewer_point_sets); + midnight_zk_stdlib::prove::( + &decider_srs, + &decider_pk, + &decider_relation, + &decider_instance, + decider_witness, + OsRng, + ) + .expect("tree decider proof generation should succeed") + }; + println!( + "[ivc-keccak-solidity] tree decider (Keccak): prove {:.2?} ({} bytes compressed)", + t0.elapsed(), + final_proof.len() + ); + + // Public input vector exactly mirrors `IvcTreeDeciderCircuit::format_instance`. + let pi: Vec = + IvcTreeDeciderCircuit::format_instance(&decider_instance).expect("format_instance"); + + // Sanity: native verifier must accept the final Keccak decider proof. + let t0 = Instant::now(); + { + let _outer_proof_layout = scoped_fewer_point_sets(outer_fewer_point_sets); + midnight_zk_stdlib::verify::( + &decider_srs.verifier_params(), + &decider_vk, + &decider_instance, + None, + &final_proof, + ) + .expect("native decider verify must accept the Keccak proof"); + } + println!( + "[ivc-keccak-solidity] native tree decider verify: OK ({:.2?})", + t0.elapsed() + ); + #[cfg(feature = "rust-verifier-trace")] + let rust_trace = { + let _outer_proof_layout = scoped_fewer_point_sets(outer_fewer_point_sets); + collect_native_midfall_trace( + &decider_srs.verifier_params(), + &decider_vk, + &pi, + &final_proof, + ) + }; + + // ---------------------------------------------------------- + // Render Halo2Verifier.sol + Halo2VerifyingKey.sol. + // ---------------------------------------------------------- + // ZkStdLib creates 2 instance columns (one committed, one + // non-committed). num_instances counts the non-committed slots. + // + // IvcTreeDeciderCircuit::format_instance(instance) = + // [leaf_chain_state_0..., leaf_chain_state_1..., fully_collapsed_final_acc]. + // The Solidity verifier consumes `fully_collapsed_final_acc` for the + // final accumulator pairing check, so pass its starting instance offset. + let num_instances = pi.len(); + let final_acc_offset = + TREE_LEAVES * Chain::format_public_input(&decider_instance.leaf_states[0]).len(); + let generator = SolidityGenerator::new( + &decider_srs, + decider_vk.vk(), + GeneratorConfig::new(num_instances, 1).with_accumulator(AccumulatorEncoding::new( + final_acc_offset, + 7, + 56, + )), + ); + assert_ivc_aggregation_quotient_manifest(&generator); + let proof_evaluation_counts = generator.proof_evaluation_counts(); + print_proof_evaluation_counts(&proof_evaluation_counts); + let gas_checkpoints_enabled = halo2_solidity_verifier::SOLIDITY_GAS_CHECKPOINTS_ENABLED; + + let t0 = Instant::now(); + let quotient_trace_enabled = + cfg!(feature = "rust-verifier-trace") || halo2_solidity_verifier::SOLIDITY_TRACE_ENABLED; + let quotient_diagnostics = RenderDiagnostics { + trace: quotient_trace_enabled, + ..RenderDiagnostics::default() + }; + let quotient_evaluator_solidity = generator + .render_quotient_evaluator(quotient_diagnostics) + .expect("render_quotient_evaluator should succeed"); + let quotient_creation_code = + compile_solidity_with_runs("ient_evaluator_solidity, SOLC_OPTIMIZE_RUNS); + let mut pin_evm = Evm::default(); + let quotient_pin_address = pin_evm.create(quotient_creation_code.clone()); + let quotient_runtime_size = pin_evm.code_size(quotient_pin_address); + let quotient_codehash = pin_evm.code_hash(quotient_pin_address); + let artifacts = generator + .render(RenderOptions { + vk: RenderVk::Separate, + quotient: RenderQuotient::ExternalPinned { + runtime_len: quotient_runtime_size, + codehash: quotient_codehash, + }, + diagnostics: RenderDiagnostics { + trace: quotient_trace_enabled, + gas_checkpoints: gas_checkpoints_enabled, + }, + }) + .expect("pinned quotient render should succeed"); + let verifier_solidity = artifacts.verifier; + let vk_solidity = artifacts.verifying_key.expect("separate render includes VK"); + let pinned_quotient_solidity = + artifacts.quotient_evaluator.expect("pinned render includes quotient evaluator"); + assert_eq!( + quotient_evaluator_solidity, pinned_quotient_solidity, + "pinning the quotient evaluator must not change the evaluator source" + ); + println!( + "[ivc-keccak-solidity] rendered quotient-separated Halo2Verifier.sol = {} bytes, Halo2VerifyingKey.sol = {} bytes, Halo2QuotientEvaluator.sol = {} bytes (quotient runtime = {} bytes, codehash = 0x{quotient_codehash:064x}; took {:.2?})", + verifier_solidity.len(), + vk_solidity.len(), + quotient_evaluator_solidity.len(), + quotient_runtime_size, + t0.elapsed() + ); + + // Persist for post-mortem inspection. + let dump_dir = format!( + "{}/target/ivc-keccak-solidity-dump", + env!("CARGO_MANIFEST_DIR") + ); + std::fs::create_dir_all(&dump_dir).ok(); + std::fs::write(format!("{dump_dir}/Halo2Verifier.sol"), &verifier_solidity).ok(); + std::fs::write(format!("{dump_dir}/Halo2VerifyingKey.sol"), &vk_solidity).ok(); + std::fs::write( + format!("{dump_dir}/Halo2QuotientEvaluator.sol"), + "ient_evaluator_solidity, + ) + .ok(); + std::fs::write(format!("{dump_dir}/proof.bin"), &final_proof).ok(); + std::fs::write( + format!("{dump_dir}/proof-evaluation-counts.txt"), + proof_evaluation_count_summary(&proof_evaluation_counts), + ) + .ok(); + let pi_bytes: Vec = pi + .iter() + .flat_map(|f| ::to_repr(f).as_ref().to_vec()) + .collect(); + std::fs::write(format!("{dump_dir}/instance.le"), &pi_bytes).ok(); + println!("[ivc-keccak-solidity] saved generated contracts under {dump_dir}"); + + // ---------------------------------------------------------- + // Compile + deploy on Prague-spec revm. + // ---------------------------------------------------------- + let t0 = Instant::now(); + let vk_creation_code = compile_solidity_with_runs(&vk_solidity, SOLC_OPTIMIZE_RUNS); + let verifier_creation_code = compile_solidity_with_runs(&verifier_solidity, SOLC_OPTIMIZE_RUNS); + let vk_creation_size = vk_creation_code.len(); + let quotient_creation_size = quotient_creation_code.len(); + let verifier_creation_size = verifier_creation_code.len(); + std::fs::write( + format!("{dump_dir}/Halo2Verifier.creation.bin"), + &verifier_creation_code, + ) + .ok(); + std::fs::write( + format!("{dump_dir}/Halo2VerifyingKey.creation.bin"), + &vk_creation_code, + ) + .ok(); + std::fs::write( + format!("{dump_dir}/Halo2QuotientEvaluator.creation.bin"), + "ient_creation_code, + ) + .ok(); + println!( + "[ivc-keccak-solidity] solc compile completed in {:.2?} (optimize-runs = {SOLC_OPTIMIZE_RUNS}, no CBOR; verifier creation bytecode = {} bytes, vk creation bytecode = {} bytes, quotient creation bytecode = {} bytes)", + t0.elapsed(), + verifier_creation_size, + vk_creation_size, + quotient_creation_size + ); + + let mut evm = Evm::default(); + let vk_address = evm.create(vk_creation_code); + let quotient_address = evm.create(quotient_creation_code); + let verifier_address = + evm.create_with_two_address_args(verifier_creation_code, vk_address, quotient_address); + let vk_runtime_size = evm.code_size(vk_address); + let quotient_runtime_size = evm.code_size(quotient_address); + let verifier_runtime_size = evm.code_size(verifier_address); + let vk_codehash = evm.code_hash(vk_address); + let quotient_codehash = evm.code_hash(quotient_address); + let verifier_codehash = evm.code_hash(verifier_address); + for (name, runtime_size) in [ + ("Halo2Verifier", verifier_runtime_size), + ("Halo2VerifyingKey", vk_runtime_size), + ("Halo2QuotientEvaluator", quotient_runtime_size), + ] { + assert!( + runtime_size <= EIP170_MAX_RUNTIME_SIZE, + "{name} runtime {runtime_size} exceeds EIP-170 limit {EIP170_MAX_RUNTIME_SIZE}" + ); + } + let contract_size_summary = format!( + "solc version: {}\n\ + solc pinned version: {PINNED_SOLC_VERSION}\n\ + solc optimize runs: {SOLC_OPTIMIZE_RUNS}\n\ + solc CBOR metadata: omitted\n\ + Halo2Verifier.sol source bytes: {}\n\ + Halo2VerifyingKey.sol source bytes: {}\n\ + Halo2QuotientEvaluator.sol source bytes: {}\n\ + Halo2Verifier creation bytecode bytes: {verifier_creation_size}\n\ + Halo2VerifyingKey creation bytecode bytes: {vk_creation_size}\n\ + Halo2QuotientEvaluator creation bytecode bytes: {quotient_creation_size}\n\ + Halo2Verifier deployed runtime bytes: {verifier_runtime_size}\n\ + Halo2VerifyingKey deployed runtime bytes: {vk_runtime_size}\n\ + Halo2QuotientEvaluator deployed runtime bytes: {quotient_runtime_size}\n\ + total deployed runtime bytes: {}\n\ + Halo2Verifier deployed runtime keccak256: 0x{verifier_codehash:064x}\n\ + Halo2VerifyingKey deployed runtime keccak256: 0x{vk_codehash:064x}\n\ + Halo2QuotientEvaluator deployed runtime keccak256: 0x{quotient_codehash:064x}\n", + solc_version().expect("pinned solc version already checked"), + verifier_solidity.len(), + vk_solidity.len(), + quotient_evaluator_solidity.len(), + verifier_runtime_size + vk_runtime_size + quotient_runtime_size + ); + std::fs::write( + format!("{dump_dir}/contract-sizes.txt"), + contract_size_summary, + ) + .ok(); + println!( + "[ivc-keccak-solidity] deployed (vk = {vk_address:?}, quotient = {quotient_address:?}, verifier = {verifier_address:?})" + ); + println!( + "[ivc-keccak-solidity] contract sizes: verifier runtime = {verifier_runtime_size} bytes, vk runtime = {vk_runtime_size} bytes, quotient runtime = {quotient_runtime_size} bytes, total runtime = {} bytes", + verifier_runtime_size + vk_runtime_size + quotient_runtime_size + ); + println!( + "[ivc-keccak-solidity] runtime hashes: verifier = 0x{verifier_codehash:064x}, vk = 0x{vk_codehash:064x}, quotient = 0x{quotient_codehash:064x}" + ); + + // ---------------------------------------------------------- + // Repack the compressed proof into EIP-2537 padded form. + // ---------------------------------------------------------- + let t0 = Instant::now(); + let repacked = generator.repack_proof(&final_proof).expect("proof repack should succeed"); + println!( + "[ivc-keccak-solidity] repacked compressed -> padded: {} -> {} bytes ({:.2?})", + final_proof.len(), + repacked.len(), + t0.elapsed() + ); + + let calldata = halo2_solidity_verifier::encode_calldata(&repacked, &pi); + println!( + "[ivc-keccak-solidity] calldata = {} bytes (pi = {} field elements)", + calldata.len(), + pi.len() + ); + std::fs::write(format!("{dump_dir}/calldata.bin"), &calldata).ok(); + + // ---------------------------------------------------------- + // Call verifyProof. Gas cap raised to 500M for the IVC verifier + // (k=19, ~20+ advice columns); the Poseidon-fixture default + // (50M) is too tight for this circuit shape. + // ---------------------------------------------------------- + match evm.try_call_with_gas(verifier_address, calldata.clone(), 5_000_000_000) { + CallOutcome::Success { + gas_used, + output, + logs, + } => { + if gas_checkpoints_enabled { + dump_gas_checkpoints(&logs, gas_used); + } + #[cfg(feature = "rust-verifier-trace")] + assert_ivc_trace_matches_native_midfall(&rust_trace, &logs); + let expected: Vec = [vec![0u8; 31], vec![1]].concat(); + assert_eq!( + output, + expected, + "verifier returned 0x{} (expected 0x...01)", + hex::encode(&output) + ); + println!( + "[ivc-keccak-solidity] PASS: IVC final Keccak proof accepted on-chain in {gas_used} gas" + ); + + let mut bad_accumulator_packing = calldata.clone(); + let first_acc_word = 4 + 0x40 + 0x20 + repacked.len() + 0x20 + final_acc_offset * 0x20; + // Accumulator limbs are packed into 56-bit chunks. The first + // word uses 224 bits, so byte 3 is the lowest unused high byte + // in the big-endian ABI word. Setting it keeps the value below + // Fr but must be rejected by the accumulator packing check. + bad_accumulator_packing[first_acc_word + 3] ^= 0x01; + assert_call_reverts( + evm.try_call_with_gas(verifier_address, bad_accumulator_packing, 5_000_000_000), + "non-canonical accumulator limb packing", + ); + + let lhs_scalar_word = first_acc_word + 4 * 0x20; + let rhs_first_word = lhs_scalar_word + 0x20; + let rhs_scalar_word = rhs_first_word + 4 * 0x20; + + let mut malformed_lhs_zero_scalar = calldata.clone(); + malformed_lhs_zero_scalar[first_acc_word + 31] ^= 0x01; + overwrite_u256_word_for_test(&mut malformed_lhs_zero_scalar, lhs_scalar_word, 0); + assert_call_reverts( + evm.try_call_with_gas(verifier_address, malformed_lhs_zero_scalar, 5_000_000_000), + "malformed LHS accumulator point with zero scalar", + ); + + let mut malformed_rhs_zero_scalar = calldata.clone(); + malformed_rhs_zero_scalar[rhs_first_word + 31] ^= 0x01; + overwrite_u256_word_for_test(&mut malformed_rhs_zero_scalar, rhs_scalar_word, 0); + assert_call_reverts( + evm.try_call_with_gas(verifier_address, malformed_rhs_zero_scalar, 5_000_000_000), + "malformed RHS accumulator point with zero scalar", + ); + + let mut bad_proof_head = calldata.clone(); + overwrite_u256_word_for_test(&mut bad_proof_head, 0x04, 0x60); + let mut bad_instances_head = calldata.clone(); + overwrite_u256_word_for_test(&mut bad_instances_head, 0x24, 0x20); + for (name, malformed_calldata) in [ + ("wrong proof ABI head", bad_proof_head), + ("wrong instances ABI head", bad_instances_head), + ] { + assert_call_reverts( + evm.try_call_with_gas(verifier_address, malformed_calldata, 5_000_000_000), + name, + ); + } + } + CallOutcome::Revert { gas_used, output } => { + panic!( + "verifier reverted at gas_used = {gas_used}, output = 0x{}", + hex::encode(&output) + ); + } + CallOutcome::Halt { gas_used, reason } => { + panic!("verifier halted at gas_used = {gas_used}, reason = {reason}"); + } + } +} + +fn assert_call_reverts(outcome: CallOutcome, context: &str) { + match outcome { + CallOutcome::Revert { .. } => {} + CallOutcome::Success { output, .. } => { + panic!( + "invalid IVC verifier call returned instead of reverting ({context}): 0x{}", + hex::encode(output) + ); + } + CallOutcome::Halt { gas_used, reason } => { + panic!("invalid IVC verifier call halted instead of reverting ({context}): gas_used = {gas_used}, reason = {reason}"); + } + } +} + +fn env_flag_enabled(name: &str) -> bool { + std::env::var(name) + .map(|value| { + matches!( + value.to_ascii_lowercase().as_str(), + "1" | "true" | "yes" | "on" + ) + }) + .unwrap_or(false) +} + +fn overwrite_u256_word_for_test(bytes: &mut [u8], start: usize, value: u64) { + bytes[start..start + 32].fill(0); + bytes[start + 24..start + 32].copy_from_slice(&value.to_be_bytes()); +} + +#[cfg(feature = "rust-verifier-trace")] +fn collect_native_midfall_trace( + params_verifier: &ParamsVerifierKZG, + vk: &MidnightVK, + pi: &[F], + proof: &[u8], +) -> Vec { + use midnight_proofs::poly::commitment::Guard as _; + + plonk::solidity_trace::start(); + + let committed_pi = [C::identity()]; + let committed_columns: [&[C]; 1] = [&committed_pi]; + let public_columns: [&[F]; 1] = [pi]; + let public_inputs: [&[&[F]]; 1] = [&public_columns]; + let mut transcript = CircuitTranscript::::init_from_bytes(proof); + + let guard = plonk::prepare::, CircuitTranscript>( + vk.vk(), + &committed_columns, + &public_inputs, + &mut transcript, + ) + .expect("native prepare succeeds while collecting trace"); + transcript + .assert_empty() + .expect("native transcript consumes proof while collecting trace"); + guard + .verify(params_verifier) + .expect("native guard verifies while collecting trace"); + + plonk::solidity_trace::take() +} + +#[cfg(feature = "rust-verifier-trace")] +fn assert_ivc_trace_matches_native_midfall( + rust_trace: &[midnight_proofs::plonk::solidity_trace::SolidityTraceEvent], + logs: &[halo2_solidity_verifier::revm::primitives::Log], +) { + let solidity_trace = parse_solidity_trace_logs(logs); + let mut rust_by_id = BTreeMap::new(); + for event in rust_trace { + assert!( + rust_by_id.insert(event.id, (event.name, event.data.clone())).is_none(), + "duplicate Rust trace id {}", + event.id + ); + } + + // The IVC verifier renders the quotient numerator block in a pinned + // Halo2QuotientEvaluator, so quotient trace ids are emitted by that helper + // while transcript, proof, PCS, and pairing trace ids stay in Halo2Verifier. + let missing = rust_by_id + .keys() + .filter(|&&id| !solidity_trace.contains_key(&id)) + .copied() + .collect::>(); + assert!( + missing.is_empty(), + "Solidity trace is missing native Rust trace ids: {missing:?}" + ); + + // The generator may emit accumulator-only diagnostics. Those are outside + // the native midfall PLONK verifier, whose source-of-truth trace ends at + // the KZG pairing inputs. + let allowed_generator_only = [29u64, 30u64]; + let unexpected = solidity_trace + .keys() + .filter(|&&id| !rust_by_id.contains_key(&id) && !allowed_generator_only.contains(&id)) + .copied() + .collect::>(); + assert!( + unexpected.is_empty(), + "Solidity trace emitted ids without native Rust oracle: {unexpected:?}" + ); + assert_required_ivc_diff_trace_coverage(&rust_by_id, &solidity_trace); + + let mut matched = 0usize; + for (id, (name, rust_data)) in rust_by_id { + let solidity_data = + solidity_trace.get(&id).expect("missing Solidity trace id was checked above"); + assert_eq!( + &rust_data, + solidity_data, + "trace mismatch id={id} name={name}: rust=0x{} solidity=0x{}", + hex::encode(&rust_data), + hex::encode(solidity_data), + ); + matched += 1; + } + + let generator_only = allowed_generator_only + .into_iter() + .filter(|id| solidity_trace.contains_key(id)) + .collect::>(); + println!( + "[ivc-keccak-solidity][trace] matched {} native Rust/Solidity trace points{}", + matched, + if generator_only.is_empty() { + String::new() + } else { + format!("; generator-only accumulator trace ids: {generator_only:?}") + } + ); +} + +#[cfg(feature = "rust-verifier-trace")] +fn assert_required_ivc_diff_trace_coverage( + rust_trace: &BTreeMap)>, + solidity_trace: &BTreeMap>, +) { + for (name, id) in [ + ("theta challenge", 7), + ("beta challenge", 8), + ("gamma challenge", 9), + ("y challenge", 10), + ("x challenge", 11), + ("x1 challenge", 13), + ("x2 challenge", 14), + ("x3 challenge", 15), + ("x4 challenge", 16), + ("quotient numerator", 36), + ("f_eval", 31), + ("final MSM commitment", 33), + ("pairing lhs input", 27), + ("pairing rhs input", 28), + ("final pairing result", 35), + ] { + assert_ivc_trace_id_present(rust_trace, solidity_trace, id, name); + } + + assert_ivc_trace_range_present( + rust_trace, + solidity_trace, + 40_000..41_000, + "PCS q_com point-set commitments", + ); + assert_ivc_trace_range_present( + rust_trace, + solidity_trace, + 41_000..42_000, + "serialized PCS point sets", + ); + assert_ivc_trace_range_present(rust_trace, solidity_trace, 60_000..61_000, "selector folds"); +} + +#[cfg(feature = "rust-verifier-trace")] +fn assert_ivc_trace_id_present( + rust_trace: &BTreeMap)>, + solidity_trace: &BTreeMap>, + id: u64, + name: &str, +) { + assert!( + rust_trace.contains_key(&id), + "Rust trace missing required {name} id {id}" + ); + assert!( + solidity_trace.contains_key(&id), + "Solidity trace missing required {name} id {id}" + ); +} + +#[cfg(feature = "rust-verifier-trace")] +fn assert_ivc_trace_range_present( + rust_trace: &BTreeMap)>, + solidity_trace: &BTreeMap>, + range: std::ops::Range, + name: &str, +) { + assert!( + rust_trace.keys().any(|id| range.contains(id)), + "Rust trace missing required {name} in id range {range:?}" + ); + assert!( + solidity_trace.keys().any(|id| range.contains(id)), + "Solidity trace missing required {name} in id range {range:?}" + ); +} + +#[cfg(feature = "rust-verifier-trace")] +fn parse_solidity_trace_logs( + logs: &[halo2_solidity_verifier::revm::primitives::Log], +) -> BTreeMap> { + let mut trace = BTreeMap::new(); + + for log in logs { + let data = log.data.data.as_ref().to_vec(); + if data.is_empty() { + continue; + } + + let topics = log.data.topics(); + assert_eq!(topics.len(), 1, "trace log must have one topic"); + let id = trace_topic_id(topics[0]); + assert!( + trace.insert(id, data).is_none(), + "duplicate Solidity trace id {id}" + ); + } + + trace +} + +#[cfg(feature = "rust-verifier-trace")] +fn trace_topic_id(topic: halo2_solidity_verifier::revm::primitives::B256) -> u64 { + let bytes = topic.as_slice(); + u64::from_be_bytes(bytes[24..32].try_into().expect("topic is 32 bytes")) +} + +/// Parse LOG1 checkpoint events emitted by `--features +/// solidity-gas-checkpoints` and print a section-level gas breakdown for the +/// final IVC verifier. +fn dump_gas_checkpoints(logs: &[halo2_solidity_verifier::revm::primitives::Log], gas_used: u64) { + let mut events: Vec<(u8, u64)> = logs + .iter() + .filter_map(|log| { + let topic = log.data.topics().first()?; + let bytes = topic.as_slice(); + // Gas checkpoints encode `(id << 248) | gas()` so the upper byte is + // the checkpoint id. Trace logs use small raw topics and therefore + // have an upper byte of zero. + let id = bytes[0]; + if id == 0 { + return None; + } + let gas = u64::from_be_bytes(bytes[24..32].try_into().ok()?); + Some((id, gas)) + }) + .collect(); + + events.sort_by(|a, b| b.1.cmp(&a.1)); + + if events.is_empty() { + println!( + "[ivc-keccak-solidity][gas] no checkpoint events found in {} LOG entries", + logs.len() + ); + return; + } + + let pcs_set_count = events + .iter() + .filter_map(|(id, _)| (*id >= 17).then_some(*id)) + .max() + .and_then(|max_id| max_id.checked_sub(21)); + + println!(); + println!("=== IVC Keccak Solidity gas-checkpoint breakdown ==="); + if let Some(n) = pcs_set_count { + println!("[ivc-keccak-solidity][gas] inferred PCS point sets = {n}"); + } + println!( + "{:>4} {:>14} {:>12} {:>7} section", + "id", "gas_left", "delta", "%" + ); + + // LOG1 with one topic and no data costs about 750 gas. Subtract one + // checkpoint from each pairwise delta so the table reflects verifier work + // instead of measurement overhead. + const CHECKPOINT_COST: u64 = 750; + + let total_billed = events[0].1.saturating_sub(events[events.len() - 1].1); + let total_real_work = total_billed.saturating_sub(events.len() as u64 * CHECKPOINT_COST); + + let mut prev_gas = events[0].1; + let cp1_gas = events[0].1; + println!( + "{:>4} {:>14} {:>12} {:>7} {}", + events[0].0, + format_u64(prev_gas), + "-", + "-", + checkpoint_name(events[0].0, pcs_set_count), + ); + + for (id, gas) in events.iter().skip(1) { + let raw_delta = prev_gas.saturating_sub(*gas); + let net_delta = raw_delta.saturating_sub(CHECKPOINT_COST); + let pct = if total_real_work > 0 { + (net_delta as f64 / total_real_work as f64) * 100.0 + } else { + 0.0 + }; + println!( + "{:>4} {:>14} {:>12} {:>6.1}% {}", + id, + format_u64(*gas), + format_u64(net_delta), + pct, + checkpoint_name(*id, pcs_set_count), + ); + prev_gas = *gas; + } + + println!(); + println!( + " cp1 gas_left = {} (verifier entry)", + format_u64(cp1_gas) + ); + println!( + " last..cp1 gas billed = {} (work between first and last checkpoint)", + format_u64(total_billed) + ); + println!( + " - measurement overhead = {} ({} checkpoints x {} gas)", + format_u64(events.len() as u64 * CHECKPOINT_COST), + events.len(), + CHECKPOINT_COST + ); + println!( + " = real section work = {}", + format_u64(total_real_work) + ); + println!( + " total tx gas_used = {} (incl. tx base + calldata + pre-cp1 + post-last)", + format_u64(gas_used) + ); +} + +fn checkpoint_name(id: u8, pcs_set_count: Option) -> String { + match id { + 1 => "entry (before VK loading)".to_string(), + 2 => "VK loading + accumulator public-input precheck".to_string(), + 3 => "VK digest + committed_pi + instance absorbs".to_string(), + 4 => "user-phase advice reads + user challenge squeezes".to_string(), + 5 => "theta squeeze + lookup multiplicities".to_string(), + 6 => "beta/gamma + permutation Z products".to_string(), + 7 => "lookup helpers + Z accumulators".to_string(), + 8 => "trash_challenge + trashcans".to_string(), + 9 => "y squeeze + quotient-limb reads".to_string(), + 10 => "evaluations + x1/x2 + f_com + x3 + q_evals + x4 + pi".to_string(), + 11 => "Lagrange + instance evaluation".to_string(), + 12 => "batched identity numerator reconstruction".to_string(), + 13 => "linearization scalar prep".to_string(), + 14 => "PCS block 6 (pairing inputs LHS/RHS)".to_string(), + 15 => "public accumulator pairing batch prep".to_string(), + 16 => "final proof ec_pairing".to_string(), + 17 => "PCS block 1 (rotation points x*omega^rot)".to_string(), + 18 => "PCS block 2 (x1 powers)".to_string(), + id if id >= 19 => { + if let Some(n) = pcs_set_count { + let idx = id - 19; + if idx < n { + return format!("PCS block 3 set {idx} (q_eval fold)"); + } + if idx == n { + return "PCS block 3 (q_com input materialization)".to_string(); + } + if idx == n + 1 { + return "PCS block 4 (f_eval Lagrange interpolation)".to_string(); + } + if idx == n + 2 { + return "PCS block 5 (final_com x4-power MSM + v)".to_string(); + } + } + format!("PCS sub-block checkpoint {id}") + } + _ => "".to_string(), + } +} + +fn format_u64(n: u64) -> String { + let s = n.to_string(); + let bytes = s.as_bytes(); + let mut out = String::with_capacity(s.len() + s.len() / 3); + for (i, b) in bytes.iter().enumerate() { + if i > 0 && (bytes.len() - i).is_multiple_of(3) { + out.push(','); + } + out.push(*b as char); + } + out +} diff --git a/proofs/solidity-verifier/tests/poseidon_fixture.rs b/proofs/solidity-verifier/tests/poseidon_fixture.rs new file mode 100644 index 000000000..58bfe73a3 --- /dev/null +++ b/proofs/solidity-verifier/tests/poseidon_fixture.rs @@ -0,0 +1,563 @@ +//! Step 8 end-to-end smoke test for the midnight-proofs Solidity +//! verifier on BLS12-381 / EIP-2537. +//! +//! The test rebuilds the same circuit the midfall poseidon fixture +//! (`midfall/proofs/solidity-verifier/fixtures/poseidon/`) was generated +//! from - `ZkStdLib`-backed `PoseidonExample` at `k = 6` - generates a +//! fresh proof against the local Filecoin SRS, renders the +//! `Halo2Verifier.sol` + `Halo2VerifyingKey.sol` pair through +//! `SolidityGenerator::render` with separate VK output, compiles them with +//! `solc`, deploys both to a Prague-spec revm (so EIP-2537 BLS12-381 +//! precompiles `0x0b` / `0x0c` / `0x0f` are routed to revm's bundled +//! implementations), encodes calldata via the generator +//! and finally calls `verifyProof`. Pass condition is `output == +//! 0x...01` (the verifier accepted the proof) AND a successful native +//! `midnight_zk_stdlib::verify` against the same proof. +//! +//! Gated behind `feature = "evm,truncated-challenges"`. Skipped automatically +//! when the host lacks the local SRS / `solc` binary; otherwise it asserts both +//! the Yul renders cleanly and the Prague-spec EVM accepts the proof. +//! +//! NOTE: this test depends on `midnight-zk-stdlib` + `midnight-circuits` +//! from the immutable Midfall revision configured in `Cargo.toml`. The +//! `[patch]` blocks redirect every `midnight-*` reference to that same +//! Git source so cargo resolves a single canonical crate copy across +//! the whole dep graph. + +#![cfg(all(feature = "evm", feature = "truncated-challenges"))] + +use std::{env, path::Path}; + +use ff::Field; +use halo2_solidity_verifier::{ + compile_solidity, pinned_solc_available, Evm, GeneratorConfig, QuotientIdentitySource, + RenderOptions, RenderVk, SolidityGenerator, +}; +use midnight_circuits::{ + hash::poseidon::PoseidonChip, + instructions::{hash::HashCPU, AssignmentInstructions, PublicInputInstructions}, +}; +use midnight_curves::Fq; +use midnight_proofs::{ + circuit::{Layouter, Value}, + plonk::Error, +}; +use midnight_zk_stdlib::{utils::plonk_api::srs_for_test, Relation, ZkStdLib, ZkStdLibArch}; +use rand::SeedableRng; +use rand_chacha::ChaCha8Rng; +use sha3::Keccak256; + +type F = Fq; +const RUN_EVM_TESTS_ENV: &str = "HALO2_SOLIDITY_RUN_EVM_TESTS"; + +#[derive(Clone, Default)] +struct PoseidonExample; + +impl Relation for PoseidonExample { + type Instance = F; + + type Witness = [F; 3]; + + fn format_instance(instance: &Self::Instance) -> Result, Error> { + Ok(vec![*instance]) + } + + fn circuit( + &self, + std_lib: &ZkStdLib, + layouter: &mut impl Layouter, + _instance: Value, + witness: Value, + ) -> Result<(), Error> { + let assigned_message = std_lib.assign_many(layouter, &witness.transpose_array())?; + let output = std_lib.poseidon(layouter, &assigned_message)?; + std_lib.constrain_as_public_input(layouter, &output) + } + + fn used_chips(&self) -> ZkStdLibArch { + ZkStdLibArch { + poseidon: true, + ..ZkStdLibArch::default() + } + } + + fn write_relation(&self, _writer: &mut W) -> std::io::Result<()> { + Ok(()) + } + + fn read_relation(_reader: &mut R) -> std::io::Result { + Ok(PoseidonExample) + } +} + +fn srs_dir() -> String { + env::var("SRS_DIR").unwrap_or_else(|_| { + concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../zk_stdlib/examples/assets" + ) + .to_string() + }) +} + +/// Step 8 end-to-end smoke. It depends on the Filecoin SRS asset, solc, and +/// Prague EIP-2537 precompile support, so default CI skips it unless +/// `HALO2_SOLIDITY_RUN_EVM_TESTS=1` is set. Run explicitly via: +/// HALO2_SOLIDITY_RUN_EVM_TESTS=1 cargo test --features +/// evm,truncated-challenges --test poseidon_fixture -- --nocapture +/// Enable Solidity trace logs with: +/// HALO2_SOLIDITY_RUN_EVM_TESTS=1 cargo test --features +/// evm,truncated-challenges,solidity-trace --test poseidon_fixture -- +/// --nocapture +#[test] +fn poseidon_renders_compiles_and_verifies() { + const K: u32 = 6; + + if !env_flag_enabled(RUN_EVM_TESTS_ENV) { + eprintln!("skipping poseidon end-to-end smoke: set {RUN_EVM_TESTS_ENV}=1 to run it"); + return; + } + + // The Filecoin SRS file is loaded by `srs_for_test` via the + // `SRS_DIR` env var. Skip the test cleanly if the asset is + // unavailable on the host (e.g. fresh checkout where the SRS + // hasn't been downloaded yet). + let srs_dir = srs_dir(); + let srs_path = format!("{srs_dir}/bls_filecoin_2p{K}"); + let fallback_srs_path = format!("{srs_dir}/bls_filecoin_2p19"); + if !Path::new(&srs_path).exists() && !Path::new(&fallback_srs_path).exists() { + eprintln!( + "skipping poseidon end-to-end smoke: SRS not found at {srs_path} or {fallback_srs_path}. \ + Set SRS_DIR or fetch the asset under midfall/zk_stdlib." + ); + return; + } + env::set_var("SRS_DIR", &srs_dir); + + let relation = PoseidonExample; + let srs = srs_for_test(&relation, Some(K)); + let vk = midnight_zk_stdlib::setup_vk(&srs, &relation); + let pk = midnight_zk_stdlib::setup_pk(&relation, &vk); + + let mut rng = ChaCha8Rng::seed_from_u64(42); + let witness: [F; 3] = core::array::from_fn(|_| F::random(&mut rng)); + let instance = as HashCPU>::hash(&witness); + + let prover_rng = ChaCha8Rng::seed_from_u64(0xdebd); + let proof = midnight_zk_stdlib::prove::( + &srs, &pk, &relation, &instance, witness, prover_rng, + ) + .expect("Proof generation should not fail"); + + // Sanity-check via the native verifier first. If this fails, the + // proof itself is broken so any Solidity-side mismatch downstream + // is meaningless. + midnight_zk_stdlib::verify::( + &srs.verifier_params(), + &vk, + &instance, + None, + &proof, + ) + .expect("native verify should accept the proof"); + + // Render Halo2Verifier.sol + Halo2VerifyingKey.sol against the + // same VK. ZkStdLib creates two instance columns (one committed, + // one non-committed); set num_committed_instances accordingly. + let num_instances = 1; + let generator = SolidityGenerator::new(&srs, vk.vk(), GeneratorConfig::new(num_instances, 1)); + assert_poseidon_quotient_manifest(&generator); + let trace_solidity = halo2_solidity_verifier::SOLIDITY_TRACE_ENABLED; + let gas_checkpoints_enabled = halo2_solidity_verifier::SOLIDITY_GAS_CHECKPOINTS_ENABLED; + let artifacts = generator + .render(RenderOptions { + vk: RenderVk::Separate, + ..RenderOptions::default() + }) + .expect("separate render should succeed"); + let verifier_solidity = artifacts.verifier; + let vk_solidity = artifacts.verifying_key.expect("separate render includes VK"); + + // Persist for post-mortem inspection. + let dump_dir = format!( + "{}/target/poseidon-fixture-dump", + env!("CARGO_MANIFEST_DIR") + ); + std::fs::create_dir_all(&dump_dir).ok(); + std::fs::write(format!("{dump_dir}/Halo2Verifier.sol"), &verifier_solidity).ok(); + std::fs::write(format!("{dump_dir}/Halo2VerifyingKey.sol"), &vk_solidity).ok(); + std::fs::write(format!("{dump_dir}/proof.bin"), &proof).ok(); + std::fs::write( + format!("{dump_dir}/instance.be"), + ::to_repr(&instance).as_ref(), + ) + .ok(); + eprintln!( + "[poseidon_fixture] proof = {} bytes, vk_solidity = {} bytes, verifier_solidity = {} bytes; \ + dumps under {dump_dir}", + proof.len(), + vk_solidity.len(), + verifier_solidity.len() + ); + + // Skip the EVM portion if the pinned solc is not available. + if !pinned_solc_available() { + eprintln!("skipping poseidon end-to-end smoke: pinned solc not available"); + return; + } + + let vk_creation_code = compile_solidity(&vk_solidity); + let verifier_creation_code = compile_solidity(&verifier_solidity); + + // Deploy on Prague-spec revm (EIP-2537 routed via blst). + let mut evm = Evm::default(); + let vk_address = evm.create(vk_creation_code); + let verifier_address = evm.create_with_address_arg(verifier_creation_code, vk_address); + + // Re-pack the prover's 48-byte compressed G1 commitments into the + // 128-byte EIP-2537 form expected by the generated Solidity. + let repacked_len = generator.repack_proof(&proof).expect("proof repack should succeed").len(); + let calldata = generator + .encode_calldata(&proof, &[instance]) + .expect("calldata encoding should succeed"); + std::fs::write(format!("{dump_dir}/calldata.bin"), &calldata).ok(); + eprintln!( + "[poseidon_fixture] calldata = {} bytes (compressed_proof={}, repacked={}, instances={})", + calldata.len(), + proof.len(), + repacked_len, + 1 + ); + + use halo2_solidity_verifier::CallOutcome; + match evm.try_call_with_gas(verifier_address, calldata, 5_000_000_000) { + CallOutcome::Success { + logs, + gas_used, + output, + } => { + if trace_solidity { + dump_trace_logs(&logs); + } + if gas_checkpoints_enabled { + dump_gas_checkpoints(&logs, gas_used); + } + let expected: Vec = [vec![0u8; 31], vec![1]].concat(); + assert_eq!( + output, + expected, + "verifier should accept the proof; gas_used = {gas_used}, output = 0x{}", + hex::encode(&output) + ); + println!("Poseidon proof verified on-chain in {gas_used} gas"); + } + CallOutcome::Revert { gas_used, output } => { + panic!( + "verifier reverted with gas_used = {gas_used}, output = 0x{}", + hex::encode(&output) + ); + } + CallOutcome::Halt { gas_used, reason } => { + panic!("verifier halted with gas_used = {gas_used}, reason = {reason}"); + } + } +} + +fn assert_poseidon_quotient_manifest(generator: &SolidityGenerator<'_>) { + let manifest = generator.quotient_identity_manifest(); + let gate_names = manifest + .entries + .iter() + .filter_map(|entry| match &entry.source { + QuotientIdentitySource::Gate { gate_name, .. } => Some(gate_name.as_str()), + _ => None, + }) + .collect::>(); + for expected in [ + "arith_gate", + "12_minus_34", + "parallel_add_gate", + "full_round_gate", + ] { + assert!( + gate_names.contains(expected), + "Poseidon manifest should include normal gate {expected}; got {gate_names:?}" + ); + } + assert!( + !gate_names.contains("partial_round_gate"), + "partial_round_gate should be represented by trash, not normal gate identities" + ); + let trash_names = manifest + .entries + .iter() + .filter_map(|entry| match &entry.source { + QuotientIdentitySource::Trash { trash_name, .. } => Some(trash_name.as_str()), + _ => None, + }) + .collect::>(); + assert_eq!(manifest.trash_identities, 1); + assert!( + trash_names.iter().any(|name| name.contains("partial_round_gate")), + "Poseidon trash manifest should name partial_round_gate; got {trash_names:?}" + ); + assert_eq!(manifest.lookup_identities, 3); + assert!( + manifest.permutation_identities > 0, + "Poseidon fixture should include copy/permutation identities" + ); +} + +fn env_flag_enabled(name: &str) -> bool { + env::var(name) + .map(|value| { + matches!( + value.to_ascii_lowercase().as_str(), + "1" | "true" | "yes" | "on" + ) + }) + .unwrap_or(false) +} + +fn dump_trace_logs(logs: &[halo2_solidity_verifier::revm::primitives::Log]) { + for log in logs { + let topic = log.data.topics()[0]; + let id = u64::from_be_bytes(topic.as_slice()[24..32].try_into().unwrap()); + let data = log.data.data.as_ref(); + if data.is_empty() { + continue; + } + let name = match id { + 1 => "vk_digest", + 2 => "num_instances", + 3 => "k", + 4 => "n_inv", + 5 => "omega", + 6 => "omega_inv", + 7 => "theta", + 8 => "beta", + 9 => "gamma", + 10 => "y", + 11 => "x", + 12 => "trash_challenge", + 13 => "x1", + 14 => "x2", + 15 => "x3", + 16 => "x4", + 17 => "x_n", + 18 => "x_n_minus_1_inv", + 19 => "l_last", + 20 => "l_blind", + 21 => "l_0", + 22 => "instance_eval", + 23 => "linearization_expected_eval", + 24 => "quotient", + 25 => "f_com", + 26 => "pi", + 27 => "pairing_lhs", + 28 => "pairing_rhs", + 29 => "acc_lhs", + 30 => "acc_rhs", + 31 => "f_eval", + 32 => "v", + 33 => "final_com", + 34 => "linearization_commitment", + 35 => "final_result", + 36 => "quotient_numerator", + 1000..=1999 => "user_challenge", + 30000..=39999 => "quotient_identity_eval", + 40000..=40999 => "pcs_q_com", + 41000..=41999 => "pcs_point_set", + 60000..=60999 => "selector_fold", + _ => "unknown", + }; + if matches!(id, 24..=30 | 33..=34 | 40000..=40999) { + eprintln!("[yul-trace] {name}"); + for (slot, word) in ["x_hi", "x_lo", "y_hi", "y_lo"].iter().zip(data.chunks_exact(32)) { + eprintln!("[yul-trace] {slot} = 0x{}", hex::encode(word)); + } + } else if matches!(id, 41000..=41999) { + eprintln!("[yul-trace] {name}[{id}]"); + for (idx, word) in data.chunks_exact(32).enumerate() { + eprintln!("[yul-trace] point[{idx}] = 0x{}", hex::encode(word)); + } + } else { + eprintln!("[yul-trace] {name}[{id}] = 0x{}", hex::encode(&data[0..32])); + } + } +} + +/// Parse LOG1 events emitted by the rendered verifier when compiled +/// with `--features solidity-gas-checkpoints` and print a per-section +/// gas-delta breakdown. +/// +/// Topic format: `(id << 248) | gas()`. `id` lives in the upper 8 +/// bits and the remaining 248 bits hold `gas()`. We discard topics +/// where the upper byte is outside `1..=31` because the trace helpers +/// (`trace_u256` / `trace_point`) emit unrelated LOG1 events with +/// small `id` topics that may collide; here we filter to our own +/// checkpoint range. The poseidon fixture is built without the +/// `solidity-trace` feature in CI, so in practice the only LOG1 +/// events are ours. +/// +/// IDs 1..=16 are the top-level section boundaries (entry, VK, +/// transcript stages, quotient, linearization, PCS, accumulator, +/// pairing). IDs 17.. sit *inside* the PCS computation block (one +/// emitted between every pair of `pcs_computations` sub-blocks; the +/// last sub-block ends at cp14) and let us attribute the 514-kg PCS +/// bucket to each emitter sub-block. The exact mapping of id->block +/// depends on the circuit layout (one emitter block per (block 1, +/// block 2, *each* set in block 3, block 4, block 5, block 6) — for +/// the Poseidon fixture with 3 point sets that's 8 emitter blocks +/// and 7 mid-PCS checkpoints (cp17..=cp23) so this dumper allocates +/// space up to id=31 to leave headroom for larger circuits. +fn dump_gas_checkpoints(logs: &[halo2_solidity_verifier::revm::primitives::Log], gas_used: u64) { + fn name_of(id: u8) -> &'static str { + match id { + 1 => "entry (before VK loading)", + 2 => "VK loading + accumulator public-input precheck", + 3 => "VK digest + committed_pi + instance absorbs", + 4 => "user-phase advice reads + user challenge squeezes", + 5 => "theta squeeze + lookup multiplicities", + 6 => "beta/gamma + permutation Z products", + 7 => "lookup helpers + Z accumulators", + 8 => "trash_challenge + trashcans", + 9 => "y squeeze + quotient-limb reads", + 10 => "evaluations + x1/x2 + f_com + x3 + q_evals + x4 + pi", + 11 => "Lagrange + instance evaluation", + 12 => "batched identity numerator reconstruction", + 13 => "linearization scalar prep", + 14 => "PCS block 6 (pairing inputs LHS/RHS)", + 15 => "public accumulator pairing batch prep", + 16 => "final ec_pairing", + // Poseidon-specific PCS sub-block layout (3 point sets): + // set 0: m=33 commits, 1 rotation + // set 1: m=5 commits, 2 rotations + // set 2: m=2 commits, 3 rotations + 17 => "PCS block 1 (rotation points x*omega^rot)", + 18 => "PCS block 2 (x1 powers)", + 19 => "PCS block 3 set 0 q_eval fold (m=33, 1 rot)", + 20 => "PCS block 3 set 1 q_eval fold (m=5, 2 rots)", + 21 => "PCS block 3 set 2 q_eval fold (m=2, 3 rots)", + 22 => "PCS block 4 (f_eval Lagrange interpolation)", + 23 => "PCS block 5 (final_com x4-power MSM + v)", + _ => "", + } + } + + let mut events: Vec<(u8, u64)> = logs + .iter() + .filter_map(|log| { + let topic = log.data.topics().first()?; + let bytes = topic.as_slice(); + // Upper byte = checkpoint id; lower 31 bytes = gas() (only + // the lowest 8 are non-zero in practice for gas values + // < 2^64). We read the lowest 8 bytes as u64. + let id = bytes[0]; + if !(1..=31).contains(&id) { + return None; + } + let gas = u64::from_be_bytes(bytes[24..32].try_into().ok()?); + Some((id, gas)) + }) + .collect(); + // Sort by execution order: gas_left is monotonically decreasing, + // so descending-gas == earliest-emitted-first. This is more + // robust than sort-by-id because the PCS sub-block ids + // (17..=21) are emitted *between* cp13 and cp14. + events.sort_by(|a, b| b.1.cmp(&a.1)); + + if events.is_empty() { + eprintln!( + "[gas-checkpoints] no checkpoint events found in {} LOG entries", + logs.len() + ); + return; + } + + eprintln!(); + eprintln!("=== gas-checkpoint breakdown (per-section deltas) ==="); + eprintln!( + "{:>4} {:>14} {:>12} {:>7} section", + "id", "gas_left", "delta", "%" + ); + + // Each checkpoint costs ~750 gas (LOG1 base 375 + 1 topic 375 + + // 0 data bytes). Subtract that from each delta so the printed + // numbers reflect "real" section work, not measurement overhead. + const CHECKPOINT_COST: u64 = 750; + + let total_billed = if events[events.len() - 1].1 < events[0].1 { + events[0].1.saturating_sub(events[events.len() - 1].1) + } else { + 0 + }; + let total_real_work = total_billed.saturating_sub(events.len() as u64 * CHECKPOINT_COST); + + let mut prev_gas = events[0].1; + let cp1_gas = events[0].1; + eprintln!( + "{:>4} {:>14} {:>12} {:>7} {}", + events[0].0, + format_u64(prev_gas), + "-", + "-", + name_of(events[0].0), + ); + + for (id, gas) in events.iter().skip(1) { + let raw_delta = prev_gas.saturating_sub(*gas); + let net_delta = raw_delta.saturating_sub(CHECKPOINT_COST); + let pct = if total_real_work > 0 { + (net_delta as f64 / total_real_work as f64) * 100.0 + } else { + 0.0 + }; + eprintln!( + "{:>4} {:>14} {:>12} {:>6.1}% {}", + id, + format_u64(*gas), + format_u64(net_delta), + pct, + name_of(*id), + ); + prev_gas = *gas; + } + + eprintln!(); + eprintln!( + " cp1 gas_left = {} (verifier entry)", + format_u64(cp1_gas) + ); + eprintln!( + " cp16..cp1 gas billed = {} (work between cp1 and cp16)", + format_u64(total_billed) + ); + eprintln!( + " - measurement overhead = {} ({} checkpoints x {} gas)", + format_u64(events.len() as u64 * CHECKPOINT_COST), + events.len(), + CHECKPOINT_COST + ); + eprintln!( + " = real section work = {}", + format_u64(total_real_work) + ); + eprintln!( + " total tx gas_used = {} (incl. tx base + calldata + pre-cp1 + post-cp16)", + format_u64(gas_used) + ); +} + +fn format_u64(n: u64) -> String { + let s = n.to_string(); + let bytes = s.as_bytes(); + let mut out = String::with_capacity(s.len() + s.len() / 3); + for (i, b) in bytes.iter().enumerate() { + if i > 0 && (bytes.len() - i).is_multiple_of(3) { + out.push(','); + } + out.push(*b as char); + } + out +} diff --git a/proofs/src/plonk/prover.rs b/proofs/src/plonk/prover.rs index ffb873deb..43006c70b 100644 --- a/proofs/src/plonk/prover.rs +++ b/proofs/src/plonk/prover.rs @@ -1101,6 +1101,7 @@ impl Assignment for WitnessCollection<'_, F> { #[test] #[cfg(feature = "dev-curves")] fn test_create_proof() { + use blake2b_simd::State; use midnight_curves::bn256::{Bn256, Fr}; use rand_core::OsRng; @@ -1139,7 +1140,7 @@ fn test_create_proof() { let params: ParamsKZG = ParamsKZG::unsafe_setup(K, OsRng); let vk = keygen_vk_with_k(¶ms, &MyCircuit, K).expect("keygen_vk should not fail"); let pk = keygen_pk(vk, &MyCircuit).expect("keygen_pk should not fail"); - let mut transcript = CircuitTranscript::<_>::init(); + let mut transcript = CircuitTranscript::::init(); // Create proof with wrong number of instances let proof = create_proof::, _, _>( diff --git a/proofs/src/poly/kzg/mod.rs b/proofs/src/poly/kzg/mod.rs index 18d0bd36f..b30940fd1 100644 --- a/proofs/src/poly/kzg/mod.rs +++ b/proofs/src/poly/kzg/mod.rs @@ -40,7 +40,8 @@ mod fewer_point_sets_runtime { ENABLED.with(Cell::get) } - /// Guard that restores the previous fewer-point-sets runtime setting when dropped. + /// Guard that restores the previous fewer-point-sets runtime setting when + /// dropped. #[derive(Debug)] pub struct ScopedFewerPointSets { previous: bool, @@ -90,8 +91,8 @@ pub fn fewer_point_sets_enabled() -> bool { /// Temporarily enables or disables KZG multi-open dummy queries on this thread. /// /// This is intentionally scoped so recursive proving can use fewer point sets -/// for proofs verified inside a circuit while an outer proof in the same process -/// can be emitted without dummy query scalars. +/// for proofs verified inside a circuit while an outer proof in the same +/// process can be emitted without dummy query scalars. pub fn scoped_fewer_point_sets(enabled: bool) -> ScopedFewerPointSets { #[cfg(feature = "fewer-point-sets")] { @@ -592,7 +593,10 @@ mod tests { query::{ProverQuery, VerifierQuery}, CommitmentLabel, EvaluationDomain, }, - transcript::{CircuitTranscript, Hashable, Sampleable, Transcript}, + transcript::{ + CircuitTranscript, Hashable, Sampleable, Transcript, TranscriptHash, + TranscriptInputBytes, + }, utils::arithmetic::eval_polynomial, }; @@ -619,6 +623,7 @@ mod tests { E::Fr: Hashable + Sampleable + Ord + Hash, E::G1: Hashable + CurveExt, E::G1Affine: CurveAffine + SerdeObject, + ::Input: TranscriptInputBytes, { let mut transcript = T::init_from_bytes(proof); @@ -668,6 +673,7 @@ mod tests { E::Fr: WithSmallOrderMulGroup<3> + Hashable + Hash + Sampleable + Ord, E::G1: Hashable + CurveExt, E::G1Affine: SerdeObject + CurveAffine, + ::Input: TranscriptInputBytes, { let k = (kzg_params.g.len() - 1).ilog2() + 1; let domain = EvaluationDomain::new(1, k); diff --git a/proofs/src/transcript/implementors.rs b/proofs/src/transcript/implementors.rs index 689c10a81..cd507d075 100644 --- a/proofs/src/transcript/implementors.rs +++ b/proofs/src/transcript/implementors.rs @@ -263,9 +263,8 @@ impl Sampleable for Fr { #[cfg(feature = "keccak-transcript")] impl Hashable for midnight_curves::G1Projective { /// Converts the point to the EIP-2537 padded uncompressed form for - /// Fiat-Shamir absorbtion: 128 bytes laid out as - /// - /// x_hi (32) || x_lo (32) || y_hi (32) || y_lo (32) + /// Fiat-Shamir absorbtion: 128 bytes laid out as `x_hi (32) || + /// x_lo (32) || y_hi (32) || y_lo (32)`. /// /// where each coord is 16 zero pad bytes followed by 48 big-endian /// bytes of the BLS12-381 base-field element. The identity point diff --git a/proofs/tests/plonk_api.rs b/proofs/tests/plonk_api.rs index 1b572de58..adeac6fbc 100644 --- a/proofs/tests/plonk_api.rs +++ b/proofs/tests/plonk_api.rs @@ -20,7 +20,9 @@ use midnight_proofs::{ kzg::{params::ParamsKZG, KZGCommitmentScheme}, Rotation, }, - transcript::{CircuitTranscript, Hashable, Sampleable, Transcript}, + transcript::{ + CircuitTranscript, Hashable, Sampleable, Transcript, TranscriptHash, TranscriptInputBytes, + }, utils::{arithmetic::Field, rational::Rational}, }; use rand_core::{CryptoRng, OsRng, RngCore}; @@ -472,6 +474,7 @@ fn plonk_api() { + Hash + SerdeObject, Scheme::Commitment: Hashable, + ::Input: TranscriptInputBytes, { let (a, instance, lookup_table) = common!(F); @@ -517,6 +520,7 @@ fn plonk_api() { + Hash + SerdeObject, Scheme::Commitment: Hashable, + ::Input: TranscriptInputBytes, { let (_, instance, _) = common!(F); let pubinputs = [instance]; @@ -541,7 +545,14 @@ fn plonk_api() { bad_keys!(Scalar, Scheme); let rng = OsRng; - let mut params = ParamsKZG::::unsafe_setup(K, rng); + let params_k = if cfg!(feature = "single-h-commitment") { + K + 1 + } else { + K + }; + let mut params = ParamsKZG::::unsafe_setup(params_k, rng); + #[cfg(feature = "single-h-commitment")] + params.downsize_lagrange(K); let pk = keygen::(&mut params); diff --git a/zk_stdlib/CHANGELOG.md b/zk_stdlib/CHANGELOG.md index a54ca3367..3929ce575 100644 --- a/zk_stdlib/CHANGELOG.md +++ b/zk_stdlib/CHANGELOG.md @@ -23,6 +23,8 @@ verification keys break backwards compatibility. * Fix cost model proof size check to account for committed instance columns [#280](https://github.com/midnightntwrk/midnight-zk/pull/280) ### Changed +* Update SRS-loading and verifier setup paths used by the Solidity verifier + integration tests. * Add `nb_arith_cols` field to `ZkStdLibArch` for configurable arithmetic columns (requires `ZKSTD_VERSION` bump before release) [#287](https://github.com/midnightntwrk/midnight-zk/pull/287) * Update goldenfiles and static VKs after logup blinding factor fix [#312](https://github.com/midnightntwrk/midnight-zk/pull/312) * bug patch in the credential property verification example, which was omitting some index computation in circuit [#240](https://github.com/midnightntwrk/midnight-zk/pull/240) diff --git a/zk_stdlib/examples/poseidon.rs b/zk_stdlib/examples/poseidon.rs index ea8716a41..fd7b322b1 100644 --- a/zk_stdlib/examples/poseidon.rs +++ b/zk_stdlib/examples/poseidon.rs @@ -72,15 +72,12 @@ fn main() { ) .expect("Proof generation should not fail"); - - assert!( - midnight_zk_stdlib::verify::( - &srs.verifier_params(), - &vk, - &instance, - None, - &proof - ) - .is_ok() + assert!(midnight_zk_stdlib::verify::( + &srs.verifier_params(), + &vk, + &instance, + None, + &proof ) + .is_ok()) } From 258814d715149b49378db38d0d5c47ebcb3f06f3 Mon Sep 17 00:00:00 2001 From: Julien Coolen Date: Tue, 19 May 2026 09:34:59 +0100 Subject: [PATCH 06/19] Add Moonlight Sepolia verifier tooling --- proofs/solidity-verifier/README.md | 40 + .../Halo2Verifier.runtime.bytecode.hex | 1 + .../sepolia/moonlight-wrap/Halo2Verifier.sol | 3572 +++++++++++++++++ .../Halo2VerifyingKey.runtime.bytecode.hex | 1 + .../moonlight-wrap/Halo2VerifyingKey.sol | 693 ++++ .../sepolia/moonlight-wrap/README.md | 93 + .../sepolia/moonlight-wrap/deployment.json | 36 + proofs/solidity-verifier/fuzz/.gitignore | 4 + proofs/solidity-verifier/fuzz/Cargo.toml | 45 + proofs/solidity-verifier/fuzz/README.md | 43 + .../fuzz/fuzz_targets/proof_repack.rs | 119 + .../fuzz/fuzz_targets/solidity_calldata.rs | 429 ++ .../scripts/deploy_moonlight_sepolia.sh | 287 ++ .../prove_and_verify_moonlight_sepolia.sh | 298 ++ proofs/solidity-verifier/src/test.rs | 1021 ++++- 15 files changed, 6644 insertions(+), 38 deletions(-) create mode 100644 proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.runtime.bytecode.hex create mode 100644 proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.sol create mode 100644 proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2VerifyingKey.runtime.bytecode.hex create mode 100644 proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2VerifyingKey.sol create mode 100644 proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md create mode 100644 proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/deployment.json create mode 100644 proofs/solidity-verifier/fuzz/.gitignore create mode 100644 proofs/solidity-verifier/fuzz/Cargo.toml create mode 100644 proofs/solidity-verifier/fuzz/README.md create mode 100644 proofs/solidity-verifier/fuzz/fuzz_targets/proof_repack.rs create mode 100644 proofs/solidity-verifier/fuzz/fuzz_targets/solidity_calldata.rs create mode 100755 proofs/solidity-verifier/scripts/deploy_moonlight_sepolia.sh create mode 100755 proofs/solidity-verifier/scripts/prove_and_verify_moonlight_sepolia.sh diff --git a/proofs/solidity-verifier/README.md b/proofs/solidity-verifier/README.md index 6602487de..9a025752d 100644 --- a/proofs/solidity-verifier/README.md +++ b/proofs/solidity-verifier/README.md @@ -307,6 +307,46 @@ proofs/solidity-verifier/target/moonlight-wrap-solidity-dump/ instance.le ``` +Deploy the generated Moonlight contracts on Sepolia: + +```bash +SEPOLIA_RPC_URL=https://... \ +scripts/deploy_moonlight_sepolia.sh \ + --account sepolia-moonlight \ + --skip-verify +``` + +The deploy script writes addresses to: + +```text +target/moonlight-wrap-sepolia-deployment.env +``` + +Create a fresh Moonlight proof and verify the generated calldata against the +deployed Sepolia verifier: + +```bash +SEPOLIA_RPC_URL=https://... \ +scripts/prove_and_verify_moonlight_sepolia.sh \ + --deployment-env target/moonlight-wrap-sepolia-deployment.env \ + --account sepolia-moonlight +``` + +To reuse an already deployed verifier directly: + +```bash +SEPOLIA_RPC_URL=https://... \ +scripts/prove_and_verify_moonlight_sepolia.sh --verifier 0x... +``` + +The verification script performs an `eth_call` by default, expecting +`verifyProof(bytes,uint256[])` to return `true`. Add `--send` if you also want +to broadcast a transaction after the successful call. + +The Sepolia deployment used during development is recorded under +`deployments/sepolia/moonlight-wrap/`, including the generated Solidity sources +and exact on-chain runtime bytecode for both the VK and verifier contracts. + Moonlight wrap can look slightly cheaper than the local IVC Keccak bench even when both runs have the same verifier proof scale. In one representative run, both had `102` proof eval scalars, `0` dummy PCS evals, `4` PCS point sets, and diff --git a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.runtime.bytecode.hex b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.runtime.bytecode.hex new file mode 100644 index 000000000..fc5f8ad6a --- /dev/null +++ b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.runtime.bytecode.hex @@ -0,0 +1 @@ +0x6108e06040526004361015610012575f80fd5b5f3560e01c80631e8e1e1314610078576342b7259714610030575f80fd5b34610074575f366003190112610074576040517f0000000000000000000000002132e1c5f2afe710e0f3fe01ec70e95000f998826001600160a01b03168152602090f35b5f80fd5b346100745760403660031901126100745760043567ffffffffffffffff8111610074573660238201121561007457806004013567ffffffffffffffff81116100745760243691830101116100745760243567ffffffffffffffff8111610074573660238201121561007457806004013567ffffffffffffffff8111610074576024369160051b8301011161007457611ec060409114911416156100745760017f0000000000000000000000002132e1c5f2afe710e0f3fe01ec70e95000f998825a8260f81b175f80a17f24430c63ec2017f12ec4004d66c66314f418dd77fe38dd4c2475c8f2b259e009813f14614281823b1416156100745781612700614280923c6121443614611ec435601314604435611e6014831616168015610074575f90731a0111ea397fe69a4b1ba7b6434bacd764774b846120a435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa612084351416731a0111ea397fe69a4b1ba7b6434bacd764774b8461206435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa6120443514161680614d3c575b15614ca8575b166080616bc061a1c05e808261a24052614c8d575b5f90731a0111ea397fe69a4b1ba7b6434bacd764774b8461212435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa612104351416731a0111ea397fe69a4b1ba7b6434bacd764774b846120e435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa6120c43514161680614c70575b15614bdc575b1680916080616c4061a1c05e61a24052614bc1575b8015610074575a600160f91b175f80a160806127005190525f60a0525f60c0525f60e0525f61010052601361012052610140611ee45b6121448110614b9d57505a600360f81b175f80a16064906191c0905b826107e48110614b80575f5160206152d85f395f51905f5285925a600160fa1b175f80a1607f190160802080608052066169805260a090619940916101008201925b838310614b625784835f5160206152d85f395f51905f52845a600560f81b175f80a1607f190160802080608052066169a0525f5160206152d85f395f51905f52602060802080608052066169c05260a0619a4061030083015b808410614b405750505a600360f91b175f80a1619d40608083015b808410614b1e575061010060806103f3858095614e62565b948181619e4037019201905b818310614afc5750505f5160206152d85f395f51905f526080610423838095614e62565b928181619ec03701915a600760f81b175f80a1607f190160802080608052066169e05260a0610100619f409301925b838310614ade5784835f5160206152d85f395f51905f52845a600160fb1b175f80a1607f19016080208060805206616a005260a090619fc0916102008201925b838310614ac05784835f5160206152d85f395f51905f52845a600960f81b175f80a1607f19016080208060805206616a205260a090618500610cc08201905b818310614a8f5750505f5160206152d85f395f51905f528192607f19016080208060805206616a40525f5160206152d85f395f51905f5260206080208060805206616a6052610120608061052483614dcc565b928181616ac0370191607f190160802092836080526001600160801b035f5160206152d85f395f51905f5260a0950616616a8052826182a052015b808210614a6757506080905f5160206152d85f395f51905f52611ec493607f1901832080845206616aa05261059381614dcc565b508181616b40370103610074578015610074575a600560f91b175f80a1616a205180915f5b60148110614a4c57506127805192616cc0846127c0515b6170608310614a2757505050506106085f5160206152d85f395f51905f525f5160206153385f395f51905f528408918261706052614ef6565b925f5160206152d85f395f51905f52616cc092612760519009916127c0515b6170608210614a02578585616ce05190616d00915b616e0083106149e5575f92611ee4905b61214482106149c057505061706051616cc05190616e005193616cc052616ce052616d0052616d2052616d4052616d60525a600b60f81b175f80a1801561007457616a0051610300525f61a300525f5b61014081106149b157506001805b603182106149885782618b4051618a40516185205190618a6051916185405161088052618a8051936185605194618aa0516108c0526185805190618ac0516185a0516108a052618ae05191886185c0519586946108805189618b0051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f529109955f5160206152d85f395f51905f529109936108a0515f5160206152d85f395f51905f52910992866108c051905f5160206152d85f395f51905f529109925f5160206152d85f395f51905f52910990610880515f5160206152d85f395f51905f52908c09905f5160206152d85f395f51905f528a8c095f5160206152d85f395f51905f529108905f5160206152d85f395f51905f529108905f5160206152d85f395f51905f529108905f5160206152d85f395f51905f529108905f5160206152d85f395f51905f529108905f5160206152d85f395f51905f529108905f5160206152d85f395f51905f5291088684618b2051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f5291085f5160206152d85f395f51905f5290600109956103005161a30051905f5160206152d85f395f51905f5291099661a1c051905f5160206152d85f395f51905f52910861a1c0526108a0515f5160206152d85f395f51905f52035f5160206152d85f395f51905f52905f08915f5160206152d85f395f51905f52035f5160206152d85f395f51905f52905f089061088051905f5160206152d85f395f51905f529108905f5160206152d85f395f51905f529108905f5160206152d85f395f51905f5291085f5160206152d85f395f51905f529060010961a1e051905f5160206152d85f395f51905f52910861a1e0525f5160206152d85f395f51905f52035f5160206152d85f395f51905f52905f08915f5160206152d85f395f51905f529108905f5160206152d85f395f51905f5291085f5160206152d85f395f51905f529060010961a20051905f5160206152d85f395f51905f529108906185e0515f5160206152d85f395f51905f52035f5160206152d85f395f51905f52905f089061088051905f5160206152d85f395f51905f529108905f5160206152d85f395f51905f5291085f5160206152d85f395f51905f5290600109918261a9605261030051906103005190610300515f5160206152d85f395f51905f529109905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f52910961a3005261a360515f5160206152d85f395f51905f529109905f5160206152d85f395f51905f52910861a20052614120905f9061a960825b84906152ef861015611f3b57600186515f1a9601959182600514611f0f575081600614611eef5781600814611ed45781600d14611eaa5781601014611e865781601114611e625781602114611bc35750806019146118785780601f146113225780601b14610b6357600b14610af2575f80fd5b600384519401935f905f5160206152d85f395f51905f526103005161a300510961a300525f5160206152d85f395f51905f5285611fe08360f31c1661a1c0019283519061ffff8160e81c16610b4b575b50089052610a7f565b90621fffe0849260e31c1661a3400151900989610b42565b505082516002909301925f925061a96090839060f01c8015610e8f5780600114610d635780600214610c7e57600314610b9a575f80fd5b5f5160206152d85f395f51905f528080808080618b0051816185e05181035f0890088180618520518161858051918009097f404d21073985d14e432a4ad76d3fae06ca74314b950fe7b1d7f501cd31a8b374099008818061854051816185a051918009097f0b2cc8704264c6bd81bc620e9e524d4b73e9b2317679422ff7fa1603955649f10990088180618560518161862051918009097f0fdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f099008600109808452816103005161a300510961a300528161a2e05161a3605190090861a2e052610a7f565b505f5160206152d85f395f51905f528080808080618ae051816185c05181035f0890088180618520518161858051918009097f1b8114c381b922fd5d6d241210e2d8a68ad5744053ba9e776118de4107b51ace099008818061854051816185a051918009097f3df32e4cc4cb2ed20e5d21899cf5331775990ccaec4c09b4e3717213fcc0d7630990088180618560518161862051918009097f3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc1099008600109808452816103005161a300510961a300528161a2e05161a3605190090861a2e052610a7f565b505f5160206152d85f395f51905f528080807f73eda753299d7d483339d80809a1d7f7b67900f7fe6bfad98998021b7bb5732b81807f73eda753299d7d483339d80809a1d80553bca402fffe5bfeffffffff0000000181808080600160701b61852051088180600160701b6185405108600160381b0990088180600160701b6185605108600160701b099008818080618660518161868051600160381b0990086186a0518290600160701b09900881035f08900808818070241eabfffeb153ffffb9feffffffffaaab6187c0510981035f089008088180600160861b817f73eda753299d7d483339d80809a1d80553bda402fffe5b6e855000003ab000026187e051080981035f089008600109808452816103005161a300510961a3005261a240510861a24052610a7f565b5061852051610400526185c051610520526185e051610480526186005161044052618540516104a0526187a0516104c0525f5160206152d85f395f51905f528080807f73eda753299d7d483339d80809a1d7d340dd972492de594de627fffefcb803498180618560516187805161054052618580516105805261876051610460526185a0516104205261874051610500526186205161056052618640516104e05281808080618660518161868051600160381b0990086186a0518290600160701b09900881035f089181808061044051600160701b09818061048051600160381b096105205108089181808083600160701b0981806104a051600160381b09610400510808918180806104c0516104e05109700c8557e86f90d0d89eed6eb5349a0f88200991818080610540516104e05109702cb9b546d20373eaf85e8f53db883cb5480991818080610460516104e0510970013af65741744bd7bb2c6872df2b8003200991818080610500516104e0510970340f2ebe380a0f5eff4360543988a61dc20991818080610440516104e0510970297784894e27525bc342b7fde37dba93660991818080610480516104e05109703212e00cde6d2002b119d800000347fcb809918180806104c0516105605109702cb9b546d20373eaf85e8f53db883cb548099181808061054051610560510970013af65741744bd7bb2c6872df2b800320099181808061046051610560510970340f2ebe380a0f5eff4360543988a61dc2099181808061050051610560510970297784894e27525bc342b7fde37dba93660991818080610440516105605109703212e00cde6d2002b119d800000347fcb809918180806104c051610420510970013af65741744bd7bb2c6872df2b800320099181808061054051610420510970340f2ebe380a0f5eff4360543988a61dc2099181808061046051610420510970297784894e27525bc342b7fde37dba93660991818080610500516104205109703212e00cde6d2002b119d800000347fcb809918180806104c051610580510970340f2ebe380a0f5eff4360543988a61dc2099181808061054051610580510970297784894e27525bc342b7fde37dba93660991818080610460516105805109703212e00cde6d2002b119d800000347fcb809918180806104c051840970297784894e27525bc342b7fde37dba936609918180808080610540518609703212e00cde6d2002b119d800000347fcb80993610520519009600160701b098180806104c0516104a05109703212e00cde6d2002b119d800000347fcb809818080610480516104a05109600160701b09818080610520516104a05109600160381b09818080610440516104005109600160701b09818080610480516104005109600160381b09816105205161040051090808080808080808080808080808080808080808080808080808080808818070241eabfffeb153ffffb9feffffffffaaab6187c0510981035f089008088180600160861b817f73eda753299d7d483339d80809a1d80553b9202d7ffe85d4800008bb200000016187e051080981035f089008600109808452816103005161a300510961a3005261a220510861a22052610a7f565b505090505f905f61a96090616d40516103a052616d005161032052616d20516103e0526169a05161038052616980516103c0525f5160206152d85f395f51905f52806190e05181610320516103a0510809816103005161a30051090861a300525f5160206152d85f395f51905f52618b6051816103c05191816103c0515f0908095f5b6004811061184b57505060015f5b600481106118295750600161a9e05260015b600481106117f75750600161aac0526003805b6117c557505f905f5b600481106117985750905f5160206152d85f395f51905f52808080808096816190c05197810391880908816103005161a3005109089381808080618be051948180618b8051816103c0515f090881618ba051916103c05190090895096190e0510881036191005108816190a0519361038051900890090881806103e05161032051088103600108099161030051900908610360526191605161034052618a00516102e05261852051610260526185405161028052618560516102c052618580516102a0526185a0516102405261862051610220526186405161020052618660516101e052618680516101c0526186a0516101a0526186c051610180526186e0516101605261870051610140526187205161012052618bc051610100526191405160e0525f5160206152d85f395f51905f5280618c005181035f0860010860c0525f5160206152d85f395f51905f52808060e05160010961034051088103619180510860a0525f5160206152d85f395f51905f5280806191205181806103805181806101005160c05109816103c05181806101205160c05109816103c05181806101405160c05109816103c05181806101605160c05109816103c05181806101805160c05109816103c05181806101a05160c05109816103c05181806101c05160c05109816103c05181806101e05160c05109816103c05181806102005160c05109816103c05181806102205160c05109816103c05181806102405160c05109816103c05181806102a05160c05109816103c05181806102c05160c05109816103c05181806102805160c05109816103c05181806102605160c05109816103c05181806102e05160c05109816103c0515f09080908090809080908090809080908090809080908090809080908090809080860a051090881806103e0516103205108810360010809816103005181805f5160206153385f395f51905f528180610380518161010051816103c0518161012051816103c0518161014051816103c0518161016051816103c0518161018051816103c051816101a051816103c051816101c051816103c051816101e051816103c0518161020051816103c0518161022051816103c0518161024051816103c051816102a051816103c051816102c051816103c0518161028051816103c0518161026051816103c051816102e051816103c0515f09080908090809080908090809080908090809080908090809080908090809080860e0510908816103005181806103405181610320516103a051080981610300516103605109080908090861a30052610a7f565b915f5160206152d85f395f51905f52600191818560051b8061aa6001519061a9e0015109900892016113e1565b5f5160206152d85f395f51905f528160051b808601519061aa600151095f19820160051b61aa6001525f1901806113d8565b6001905f5160206152d85f395f51905f525f19820160051b808701519061a9e00151098160051b61a9e00152016113c5565b905f5160206152d85f395f51905f526001918360051b860151900991016113b3565b8060019160051b5f5160206152d85f395f51905f52610380518183618540015187080890860152016113a5565b5050618a205161a9609081525f925082805b60058110611baa57506185005161aa2052616d605161aa40525f5b60098110611b915750618a005161ab80525f5b60128110611b7857505f5b60068110611b5d57505f5b60068110611b4257505f5b60058110611b2757505f5160206152d85f395f51905f5280808061ade0518103600108616d405109816103005161a30051090881808061ae80518181810391800908616d005109916103005190090861a3005260015b60068110611ae157505f5160206152d85f395f51905f52616a20516169a0510961b000525f5b600681106119635750610a7f565b600381026003810160128111611ad9575b8260051b908161aea001519161ade001519061b00051908194906169c051906169a0515b8a828510611a0c57505050505050600193925f5160206152d85f395f51905f52808080957f4285088329c399ea457a8ca1d30f8957e74c7f529842a1579b4fee55b398292395820390088180616d2051616d0051088103890809816103005161a30051090861a300520961b0005201611955565b9285979385969792939482809760051b809301519261aba001515f5160206152d85f395f51905f529087095f5160206152d85f395f51905f52908408905f5160206152d85f395f51905f5291085f5160206152d85f395f51905f529109985f5160206152d85f395f51905f529108905f5160206152d85f395f51905f5291085f5160206152d85f395f51905f529109947f08634d0aa021aaf843cab354fabb0062f6502437c6a09c006c083479590189d75f5160206152d85f395f51905f52910993600101929190611998565b506012611974565b805f5160206152d85f395f51905f52808060019460051b61ade001515f19850160051b61af60015182039008616d405109816103005161a30051090861a300520161192f565b80606060019202618ec001518160051b61af600152016118d9565b80606060019202618ea001518160051b61aea00152016118ce565b80606060019202618e8001518160051b61ade00152016118c3565b8060019160051b80618c4001519061aba00152016118b8565b8060019160051b8061862001519061aa600152016118a5565b8060019160051b8061852001519061a98001520161188a565b92939480915090600181515f1a91015f9260018316611e52575b505f9360028316611e39575b815196875f1a8860011a908960021a9a60058b60041a960199611e2b575b505f5b818110611dde5750505f5b818110611d7f5750505f5b878a821015611cb75788519860118a60f01c9101995f5b60078110611c4b5750505050600101611c20565b8060051b8301515f6080525b600760805110611c6a5750600101611c37565b9a5f5160206152d85f395f51905f5290818d81600460805187018a0101515f1a60051b612ae001519160805160051b61ffff8960e01c16015190090990089a600160805101608052611c57565b5050959750955f905b8060031a8210611d455750505f905b808210611d01575050600116611ce9575b50916001610a7f565b905f5160206152d85f395f51905f5291510984611ce0565b90935f5160206152d85f395f51905f526001918160058b519b019a81815f1a60051b612ae001519161ffff808260d81c16519160e81c165109099008940190611ccf565b90945f5160206152d85f395f51905f526001918160038c519c019b61ffff8160e81c1651905f1a60051b612ae00151099008950190611cc0565b6002895160f01c990198515f905b8a60078310611da157505050600101611c15565b600191929a5f5160206152d85f395f51905f5260038193519e019d8161ffff825f1a60051b612ae001519260e81c16518709099008990190611d8d565b5f5b8a60078210611df3575050600101611c0a565b6001919a5f5160206152d85f395f51905f5260038193519e019d61ffff8160e81c1651905f1a60051b612ae001510990089901611de0565b84526020909301928b611c07565b9350600181515f1a91019060051b612ae0015193611be9565b905160f01c925060030187611bdd565b935f5160206152d85f395f51905f5291506002865160f01c96019551900992610a7f565b935f5160206152d85f395f51905f5291506002865160f01c96019551900892610a7f565b935f5160206152d85f395f51905f529150600186515f1a96019560051b612ae00151900992610a7f565b935f5160206152d85f395f51905f52809250035f0892610a7f565b93915f5160206152d85f395f51905f529150601f19019182510892610a7f565b92949150946003905160f01c920194611f2d575b5051916001610a7f565b835260209092019184611f23565b836169e051610840525f5160206152d85f395f51905f52618ae051815f5160206153385f395f51905f526185c051099008610720526185205161082052618540516108005261856051610760525f5160206152d85f395f51905f526107605161076051096107e05261858051610860525f5160206152d85f395f51905f52610860516108605109610740526185a0516107a0525f5160206152d85f395f51905f526107a0516107a051096107805261862051610700525f5160206152d85f395f51905f526107005161070051096107c052618640516106e0525f5160206152d85f395f51905f526106e0516106e051096106c052618660516106a0525f5160206152d85f395f51905f526106a0516106a05109610680525f5160206152d85f395f51905f52618b0051815f5160206153385f395f51905f526185e05109900861066052618b205161064052618b405161062052618a405161060052618a60516105e052618a80516105c0525f5160206152d85f395f51905f52618aa051815f5160206153385f395f51905f52618600510990086105a0525f5160206152d85f395f51905f5280808080618c205181036001086191a0519009810381808080806106805161068051096106a051095f5160206152f85f395f51905f5209818080806106c0516106c051096106e051095f5160206153585f395f51905f5209818080806107c0516107c0510961070051095f5160206152b85f395f51905f5209818080806107805161078051096107a051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e098180808061074051610740510961086051097ed94c46a8456352aa44d7a885ab59e3a36664e6fb25e826f8a4cd79822f053309818080806107e0516107e0510961076051097f1f61345b652161410c5e29f51e301ae56342af824bc110649393d2b911c50d3e098180610800517f40fa389feb2522bb934881ac9ed749aee2296502af592418c6b5675c0f560261098180610820517f70d8f2a733a64d650faccc9b1c2a766a9544bb3ff1a11ee73cb43947ef386633096105a0510808080808080808816108405181808080806106c0516106c051096106e051095f5160206152f85f395f51905f5209818080806107c0516107c0510961070051095f5160206153585f395f51905f5209818080806107805161078051096107a051095f5160206152b85f395f51905f52098180808061074051610740510961086051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e09818080806107e0516107e0510961076051097ed94c46a8456352aa44d7a885ab59e3a36664e6fb25e826f8a4cd79822f0533098180610800517f27e7119226c42a6d19c1541904b99ae40685511ed2e078964b74594d38340849098180610820517f6d05a41959f539a7fc9ec0972ea1e3dbb6fc67dd51daf3414f7fbbb091c7274a0981805f5160206153385f395f51905f526106a051096105c0510808080808080808816108405181808080806107c0516107c0510961070051095f5160206152f85f395f51905f5209818080806107805161078051096107a051095f5160206153585f395f51905f52098180808061074051610740510961086051095f5160206152b85f395f51905f5209818080806107e0516107e0510961076051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e098180610800517f23a6684b942d726a22e4d5b8d8ff83aeaa773f62600184efe5d033d7c7c6e827098180610820517f2f5908b169c6cf1bd26dcf0f9e5105481f5164f3ece0582bf3098312167751a70981805f5160206153385f395f51905f526106e051096105e05108080808080808816108405181808080806107805161078051096107a051095f5160206152f85f395f51905f52098180808061074051610740510961086051095f5160206153585f395f51905f5209818080806107e0516107e0510961076051095f5160206152b85f395f51905f52098180610800517f24822e1af9aa2887c912c87eb0f20bd332330e7e55cd784de67cb407a9f05520098180610820517f726df1506749848155630b86ae25a82b281ecd050fe3a52d85a181fa87202e4b0981805f5160206153385f395f51905f526107005109610600510808080808088161084051818080808061074051610740510961086051095f5160206152f85f395f51905f5209818080806107e0516107e0510961076051095f5160206153585f395f51905f52098180610800517f26c2cc87f95726b28f33ca03409a460ec987cfe12adae32769e3565865d07191098180610820517f222e83e70453dfee19b402e9fa8dfe2c4987b034d0be3ceb478b3022e97934c10981805f5160206153385f395f51905f526107a05109610620510808080808816108405181808080806107e0516107e0510961076051095f5160206152f85f395f51905f52098180610800517f6bd72f9cfc53af9d931896e77ea5c61244cb6d5fae8954f37dc7b9002f5aa78a098180610820517f5e1d3dbecda6214343e24a47f45c5d033197ad01b65a730af95dc57e90c491400981805f5160206153385f395f51905f5261086051096106405108080808816108405181808080806106805161068051096106a051097f0fdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f09818080806106c0516106c051096106e051097f301cf56f9b4577112cc4241cddf6484aaadedbf1bbd0f2351adf2e41c2fb2ecd09818080806107c0516107c0510961070051097f5f3a15bab4ce4097b1edc3a25002694b92395ce355a8a12fe557459d9633f70109818080806107805161078051096107a051097f275a20361ea91992193920270d3e2d1f6361880ac0a439c64bef815d4469ba85098180808061074051610740510961086051097f31e823a45e567484c1544e310c0fa5cd66547a8f0dde659ac61698c30e838d2509818080806107e0516107e0510961076051097f26cc223e16f47c20e17cc6069605fa5a8af05ea4f6eb36029a641d23b818eb10098180610800517f4d0ea7f9c3fda06d9535b0fdafd8338bd47c2200b284fa71a325ff41ac358028098180610820517f5b1fc262a28cbb8bf75d9b1a6edaa74591ec24cd9a209512213cec3a3c0f1a5d09610660510808080808080808816108405181808080806106805161068051096106a051097f3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc109818080806106c0516106c051096106e051097f06ccb1c7d87f3c12a2bde4e68ac7f1e8b03481ba15d7f88f9a7f9b8310dd6d3409818080806107c0516107c0510961070051097f53fded36d490ba6b05a5d10fd99ffe5456baec6a6a8753199d5ebdc33c99790e09818080806107805161078051096107a051097f412c98232b6ab8a47aa76ee814ef7ec6261987c9802f2cfc490e007951a60ca5098180808061074051610740510961086051097f333f8046ece5579cbd6872449c57f2703dfc8864cfadc06d587ff104a0d0c1f209818080806107e0516107e0510961076051097f3509dd2fe3aac0080783557fec090fb1cb4b2b0901253c55282024331d1fe1a8098180610800517f52f789e4afc3801f7411102ee2f47cc5954a744e71cac98e75ea962a55a0a76f098180610820517f590ba402032e82eb1f660ef09796c5686345a5054ed96dae8e2d2336337887710961072051080808080808080881610840515f0908090809080908090809080908090808816103005161a3005109088161a9405161a1c0510961a1c0528161a9205161a1e0510961a1e0528161a8c05161a200510961a200528161a8605161a220510961a220528161a8005161a240510961a240528161a7a05161a260510961a260528161a7405161a280510961a280528161a6e05161a2a0510961a2a0528161a6805161a2c0510961a2c0528161a5c05161a2e0510961a2e05281035f08616d80525a600360fa1b175f80a1616a2051908160015f5b6014811061496557505f5160206152d85f395f51905f5280808080888180988198616da0528103600108616dc0525a600d60f81b175f80a181808061278051816127a051809c81809c81809c818c819d9b829c9a839b61704052820961706052098061702052090909090909090909617000525a601160f81b175f80a1616a4051600161738052600190617380905f915b602a831061493957845a600960f91b175f80a1618a0061a3005261850061a320526190a061a340526190c061a3605261912061a3805261914061a3a0526191a061a3c052618a2061a3e052618a4061a40052618a6061a42052618a8061a44052618aa061a46052618ac061a48052618ae061a4a052618b0061a4c052618b2061a4e052618b4061a50052618b6061a52052618b8061a54052618ba061a56052618bc061a58052618be061a5a052618c0061a5c052618c2061a5e052618c4061a60052618c6061a62052618c8061a64052618ca061a66052618cc061a68052618ce061a6a052618d0061a6c052618d2061a6e052618d4061a70052618d6061a72052618d8061a74052618da061a76052618dc061a78052618de061a7a052618e0061a7c052618e2061a7e052618e4061a80052618e6061a82052616d8061a84052618a00516173a061a32060015b602b811061490e57505050617ba0525a601360f81b175f80a16186e0515f5160206152d85f395f51905f526189a0519181806173a051928184618700510990089381836189c05109900882806173c051958187618720510990089181866189e05109900890617bc052617be0525a600560fa1b175f80a1818080619060518180619080519281886190e051099008956191005109900892818661916051099008936191805109900890617c0052617c20525a601560f81b175f80a161852061a300526185c061a3205261884061a3405261854061a360526185e061a3805261886061a3a05261856061a3c05261860061a3e05261888061a4005261858061a4205261874061a440526188a061a460526185a061a4805261876061a4a0526188c061a4c05261862061a4e05261878061a500526188e061a5205261864061a540526187a061a5605261890061a5805261866061a5a0526187c061a5c05261892061a5e05261868061a600526187e061a6205261894061a640526186a061a6605261880061a6805261896061a6a0526186c061a6c05261882061a6e05261898061a70052618520516185c05161884051916173a061a36060015b600b81106148c557505050617c4052617c6052617c80525a600b60f91b175f80a1618e8061a30052618ea061a32052618ec061a34052618ee061a36052618f0061a38052618f2061a3a052618f4061a3c052618f6061a3e052618f8061a40052618fa061a42052618fc061a44052618fe061a4605261900061a4805261902061a4a05261904061a4c052618e8051618ea051618ec051916173a061a36060015b6005811061487c57505050617ca052617cc052617ce0525a601760f81b175f80a1616a6051616a80516182a0516170005191836170205181617040519581617060519188815f5160206152d85f395f51905f52035f5160206152d85f395f51905f529089085f5160206152d85f395f51905f528581038a085f5160206152d85f395f51905f528381038b08905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f529109915f5160206152d85f395f51905f5281810383085f5160206152d85f395f51905f5286810384085f5160206152d85f395f51905f5290600109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52908409895f5160206152d85f395f51905f5283810388085f5160206152d85f395f51905f5285810389085f5160206152d85f395f51905f5290600109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f5290830994878d5f5160206152d85f395f51905f5282810387085f5160206152d85f395f51905f5288810388085f5160206152d85f395f51905f5290600109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f529089096130bd90614d59565b955f5160206152d85f395f51905f5283810382085f5160206152d85f395f51905f5289810383085f5160206152d85f395f51905f5290600109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52908809935f5160206152d85f395f51905f5282810385085f5160206152d85f395f51905f528a810386085f5160206152d85f395f51905f5290600109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f529086095f5160206152d85f395f51905f528381038b085f5160206152d85f395f51905f528681038c085f5160206152d85f395f51905f5290600109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52908209995f5160206152d85f395f51905f5286810389085f5160206152d85f395f51905f528281038a08905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52908c099a8b945f5160206152d85f395f51905f52035f5160206152d85f395f51905f52908a085f5160206152d85f395f51905f529109905f5160206152d85f395f51905f52035f5160206152d85f395f51905f529089085f5160206152d85f395f51905f529082099788965f5160206152d85f395f51905f52035f5160206152d85f395f51905f5291085f5160206152d85f395f51905f529109915f5160206152d85f395f51905f52910981617ca051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f5203935f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52916080013509905f5160206152d85f395f51905f529108925f5160206152d85f395f51905f52910990617cc051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52035f5160206152d85f395f51905f529108925f5160206152d85f395f51905f52910990617ce051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52035f5160206152d85f395f51905f5291085f5160206152d85f395f51905f52825f09905f5160206152d85f395f51905f52910887895f5160206152d85f395f51905f5285810388085f5160206152d85f395f51905f5282810389085f5160206152d85f395f51905f5290600109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f528881038b085f5160206152d85f395f51905f528781038c085f5160206152d85f395f51905f528481038d08905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291098a5f5160206152d85f395f51905f528a810385085f5160206152d85f395f51905f5289810386085f5160206152d85f395f51905f5290600109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f5290830991885f5160206152d85f395f51905f528c810382085f5160206152d85f395f51905f5287810383085f5160206152d85f395f51905f5290600109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52908509968c5f5160206152d85f395f51905f52878a0961358490614d59565b965f5160206152d85f395f51905f52908809935f5160206152d85f395f51905f5282810385085f5160206152d85f395f51905f528a810386085f5160206152d85f395f51905f5290600109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f529086095f5160206152d85f395f51905f528381038b085f5160206152d85f395f51905f528681038c085f5160206152d85f395f51905f5290600109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52908209995f5160206152d85f395f51905f5286810389085f5160206152d85f395f51905f528281038a08905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52908c099a8b945f5160206152d85f395f51905f52035f5160206152d85f395f51905f52908a085f5160206152d85f395f51905f529109905f5160206152d85f395f51905f52035f5160206152d85f395f51905f529089085f5160206152d85f395f51905f529082099788965f5160206152d85f395f51905f52035f5160206152d85f395f51905f5291085f5160206152d85f395f51905f529109915f5160206152d85f395f51905f52910981617c4051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f5203935f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52916060013509905f5160206152d85f395f51905f529108925f5160206152d85f395f51905f52910990617c6051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52035f5160206152d85f395f51905f529108925f5160206152d85f395f51905f52910990617c8051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52035f5160206152d85f395f51905f529108915f5160206152d85f395f51905f529109905f5160206152d85f395f51905f529108905f5160206152d85f395f51905f5281810389085f5160206152d85f395f51905f52906001095f5160206152d85f395f51905f5282810388085f5160206152d85f395f51905f528a81038908905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5289810382085f5160206152d85f395f51905f52906001095f5160206152d85f395f51905f5290830961393790614d59565b5f5160206152d85f395f51905f528a810383085f5160206152d85f395f51905f52906001095f5160206152d85f395f51905f52908209915f5160206152d85f395f51905f528181038c085f5160206152d85f395f51905f52906001095f5160206152d85f395f51905f52908409905f5160206152d85f395f51905f528c81038b085f5160206152d85f395f51905f529083099384925f5160206152d85f395f51905f528381038d085f5160206152d85f395f51905f529109915f5160206152d85f395f51905f52035f5160206152d85f395f51905f52908c085f5160206152d85f395f51905f528e81038d08905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52910981617c0051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f5203915f5160206152d85f395f51905f5291095f5160206152d85f395f51905f529060408c013509905f5160206152d85f395f51905f529108925f5160206152d85f395f51905f52910990617c2051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52035f5160206152d85f395f51905f529108915f5160206152d85f395f51905f529109905f5160206152d85f395f51905f529108905f5160206152d85f395f51905f5281810387085f5160206152d85f395f51905f52906001095f5160206152d85f395f51905f5282810386085f5160206152d85f395f51905f528881038708905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52828209925f5160206152d85f395f51905f5289810382085f5160206152d85f395f51905f52906001095f5160206152d85f395f51905f52908509613bd890614d59565b915f5160206152d85f395f51905f528a810383085f5160206152d85f395f51905f52906001095f5160206152d85f395f51905f52908409935f5160206152d85f395f51905f52908509935f5160206152d85f395f51905f528b81038a085f5160206152d85f395f51905f529086099485935f5160206152d85f395f51905f52035f5160206152d85f395f51905f52908b085f5160206152d85f395f51905f529109915f5160206152d85f395f51905f52910981617bc051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f5203915f5160206152d85f395f51905f5291095f5160206152d85f395f51905f529060208a013509905f5160206152d85f395f51905f529108925f5160206152d85f395f51905f52910990617be051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52035f5160206152d85f395f51905f529108915f5160206152d85f395f51905f529109905f5160206152d85f395f51905f529108925f5160206152d85f395f51905f52035f5160206152d85f395f51905f529108613d9990614d59565b90617ba0515f5160206152d85f395f51905f52039035905f5160206152d85f395f51905f529108905f5160206152d85f395f51905f529109915f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291089081616e40525a600360fb1b175f80a1616aa05180808094616da051616dc051916182a051925f5160206152d85f395f51905f52856001096001600160801b038116955f5160206152d85f395f51905f5291096001600160801b038116965f5160206152d85f395f51905f5291096001600160801b038116995f5160206152d85f395f51905f5291096001600160801b038116975f5160206152d85f395f51905f5291096001600160801b03169260806198c061a3005e600161a38052608061994061a3a05e6173c05161a420526080619d4061a4405e6173e05161a4c05260806199c061a4e05e6174005161a560526080619dc061a5805e6174205161a600526080619f4061a6205e6174405161a6a052608061578061a6c05e6174605161a74052608061550061a7605e6174805161a7e052608061558061a8005e6174a05161a88052608061560061a8a05e6174c05161a92052608061568061a9405e6174e05161a9c052608061570061a9e05e6175005161aa6052608061530061aa805e6175205161ab0052608061538061ab205e6175405161aba052608061540061abc05e6175605161ac4052608061548061ac605e6175805161ace052608061580061ad005e6175a05161ad8052608061588061ada05e6175c05161ae2052608061590061ae405e6175e05161aec052608061598061aee05e6176005161af60526080615b8061af805e6176205161b000526080615f0061b0205e6176405161b0a052608061600061b0c05e6176605161b14052608061608061b1605e6176805161b1e052608061610061b2005e6176a05161b28052608061618061b2a05e6176c05161b32052608061620061b3405e6176e05161b3c052608061628061b3e05e6177005161b46052608061630061b4805e6177205161b50052608061638061b5205e6177405161b5a052608061640061b5c05e6177605161b64052608061648061b6605e6177805161b6e052608061650061b7005e6177a05161b78052608061658061b7a05e6177c05161b82052608061660061b8405e6177e05161b8c052608061668061b8e05e6178005161b96052608061670061b9805e6178205161ba0052608061678061ba205e6178405161baa052608061680061bac05e6178605161bb4052608061688061bb605e6178805161bbe052608061690061bc005e6178a05161bc80526178c051915f5160206152d85f395f51905f529083096080619fc061bca05e818161bd20525f5160206152d85f395f51905f529109608061a04061bd405e818161bdc0525f5160206152d85f395f51905f52910990608061a0c061bde05e8161be6052608061a14061be805e5f5160206152d85f395f51905f52910961bf00526080615a0061bf205e61a1c0515f5160206152d85f395f51905f5290820961bfa0526080615a8061bfc05e61a1e0515f5160206152d85f395f51905f5290820961c040526080615b0061c0605e61a200515f5160206152d85f395f51905f5290820961c0e0526080615c0061c1005e61a220515f5160206152d85f395f51905f5290820961c180526080615c8061c1a05e61a240515f5160206152d85f395f51905f5290820961c220526080615d0061c2405e61a260515f5160206152d85f395f51905f5290820961c2c0526080615d8061c2e05e61a280515f5160206152d85f395f51905f5290820961c360526080615e0061c3805e61a2a0515f5160206152d85f395f51905f5290820961c400526080615e8061c4205e61a2c0515f5160206152d85f395f51905f5290820961c4a0526080615f8061c4c05e61a2e0515f5160206152d85f395f51905f52910961c54052608061974061c5605e8361c5e05260806197c061c6005e836173a051905f5160206152d85f395f51905f52910961c68052608061984061c6a05e836173c051905f5160206152d85f395f51905f52910961c720526080619cc061c7405e8461c7c0526080619e4061c7e05e846173a051905f5160206152d85f395f51905f52910961c860526080619ec061c8805e846173c051905f5160206152d85f395f51905f52910961c9005260806191c061c9205e8761c9a052608061924061c9c05e876173a051905f5160206152d85f395f51905f52910961ca405260806192c061ca605e876173c051905f5160206152d85f395f51905f52910961cae052608061934061cb005e876173e051905f5160206152d85f395f51905f52910961cb805260806193c061cba05e8761740051905f5160206152d85f395f51905f52910961cc2052608061944061cc405e8761742051905f5160206152d85f395f51905f52910961ccc05260806194c061cce05e8761744051905f5160206152d85f395f51905f52910961cd6052608061954061cd805e8761746051905f5160206152d85f395f51905f52910961ce005260806195c061ce205e8761748051905f5160206152d85f395f51905f52910961cea052608061964061cec05e876174a051905f5160206152d85f395f51905f52910961cf405260806196c061cf605e876174c051905f5160206152d85f395f51905f52910961cfe0526080619a4061d0005e8561d080526080619ac061d0a05e856173a051905f5160206152d85f395f51905f52910961d120526080619b4061d1405e856173c051905f5160206152d85f395f51905f52910961d1c0526080619bc061d1e05e856173e051905f5160206152d85f395f51905f52910961d260526080619c4061d2805e8561740051905f5160206152d85f395f51905f52910961d300526080616ac061d3205e868261d3a052614853575b5f5160206152d85f395f51905f5296979387809693948180808080809a8199099c60808601350999606085013509966040840135099360208301350990350808080808616e60525a601960f81b175f80a16080616b40616f005e6080612860815e805f5160206152d85f395f51905f52616e605181035f086101005261483c575b806080616e806101005e614824575b6080616b406101005e80616a80516101805261480b575b806147f3575b608080616f805e5a600760f91b175f80a17c70616972696e672d62617463682d6163632d6b7a670000000000000000610100526080616f806101205e6080616f006101a05e6080616c406102205e6080616bc06102a05e5f5160206152d85f395f51905f5261022061010020069081156147ea575b6080616c406101005e8082610180526147d1575b806080616f806101805e6147b7575b80916080616bc06101005e6101805261479e575b806080616f006101805e614784575b5a600f60f81b175f80a180156100745761476e9061501d565b505a600160fc1b175f80a1600160805260206080f35b506080616f0061010080600b61c350fa60803d1416614755565b50608061010060a081600c61f230fa60803d1416614746565b506080616f8061010080600b61c350fa60803d1416614732565b50608061010060a081600c61f230fa60803d1416614723565b6001915061470f565b5060808061010081600b61c350fa60803d141661469a565b50608061010060a081600c61f230fa60803d1416614694565b5060808061010081600b61c350fa60803d141661467d565b5060808060a081600c61f230fa60803d141661466e565b9692955090926080616e806130c061a300600c6208c678fa3d60801416959296939190936145ed565b909194606060205f5160206152d85f395f51905f526001938180808c9b9a9b519b818d8c515109900899818c878c01515109900899604089015151099008970193019101612ef4565b909194606060205f5160206152d85f395f51905f526001938180808c9b9a9b519b818d8c515109900899818c878c01515109900899604089015151099008970193019101612e54565b9091926020805f5160206152d85f395f51905f52600193818851885151099008950193019101612c9c565b5f5160206152d85f395f51905f52826020600193019509926001600160801b0384168552019192612b4f565b91905f5160206152d85f395f51905f528086818460019509099280099201612abe565b5f5160206152d85f395f51905f5260019161030051900991828160051b61a340015201906106aa565b5f61a1c082015260200161069c565b909360205f5160206152d85f395f51905f52819281883586510990089501910161064c565b5f5160206152d85f395f51905f526020918451900892019161063c565b5f5160206152d85f395f51905f52838282806020958751098809855209910190610627565b602091815f5160206152d85f395f51905f5280938103870885520991019085906105cf565b915f5160206152d85f395f51905f52816001920992016105b8565b9181355f5160206152d85f395f51905f5281101561007457815260209081019291019061055f565b90928235915f5160206152d85f395f51905f52831015610074578281529181526020908101939281019291016104d1565b91906080614acf838293614e62565b93818482370191019190610492565b91906080614aed838293614e62565b93818482370191019190610452565b9190926080614b0c838293614e62565b938184823701910192909291926103ff565b916080614b2f858293969496614e62565b9481848237019101919291926103db565b916080614b51858293969496614e62565b9481848237019101919291926103c0565b91906080614b71838293614e62565b93818482370191019190610367565b614b8e608092918392614e62565b92818582370192019190610325565b90602080918335945f5160206152d85f395f51905f52861016948152019101610309565b506080616c4060a061a1c0600c61f230fa60803d14166102d3565b9050614bf2600160381b600760386120c46150ff565b9392614c08600160381b60076038612104615248565b5093919092169580614c4c575b15614c24575b505050506102be565b909192948383178287171715151694616c4052616c6052616c8052616ca05283808080614c1b565b95838317828617171516955f616c40525f616c60525f616c80525f616ca052614c15565b915082915f616c40525f616c60525f616c80525f616ca0526102b8565b506080616bc060a061a1c0600c61f230fa60803d1416610235565b9050614cbe600160381b600760386120446150ff565b9392614cd4600160381b60076038612084615248565b5093919092169580614d18575b15614cf0575b50505050610220565b909192948383178287171715151694616bc052616be052616c0052616c205283808080614ce7565b95838317828617171516955f616bc0525f616be0525f616c00525f616c2052614ce1565b915082915f616bc0525f616be0525f616c00525f616c205261021a565b801561007457602061260052602061262052602061264052612660527f73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffffff612680525f5160206152d85f395f51905f526126a052602061260060c08160055afa156100745760203d03610074576126005190565b80356040820135908060801c610074578160801c610074576001600160801b038091169116906f1a0111ea397fe69a4b1ba7b6434bacd75f5160206153185f395f51905f52602085013511158183141691101715610074576f1a0111ea397fe69a4b1ba7b6434bacd75f5160206153185f395f51905f526060840135111581831416911017156100745760809060a03761012090565b81356040830135908060801c610074578160801c610074576001600160801b038091169116906f1a0111ea397fe69a4b1ba7b6434bacd75f5160206153185f395f51905f52602086013511158183141691101715610074576f1a0111ea397fe69a4b1ba7b6434bacd75f5160206153185f395f51905f52606085013511158183141691101715610074576080809282370190565b801561501a575061a1c090616cc05191616ce0925b6170608410614ff55783515f5160206152d85f395f51905f5291098015614fee57602082526020808301526020604083015260608201527f73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffffff60808201525f5160206152d85f395f51905f5260a082015260208160c08160055afa60203d141692815191601f1901905b80616ce010614fc65750505f5160206152d85f395f51905f5280616ce051830991616cc051900990616cc052616ce052565b5f5160206152d85f395f51905f5280835185099382519009928152601f199182019101614f94565b505f925050565b60205f5160206152d85f395f51905f5281928694965190099283865201930190614f0b565b90565b801561501a57506080616f806103005e6101006128e06103805e6080616f006104805e6101006129e06105005e60206103008080600f62029810fa60203d141661030051161561007457600190565b5f96945f969293945f1901925f5b86811061508a5750505050505050565b8483820483808260051b8801359215166150f7575b5087858406021c168682026101008110806150d6575b156150c5575b505060010161507a565b60ff19011b9099019860015f6150bb565b9a82821b019a61010089830111156150b5579b8282610100031c019b6150b5565b90038361509f565b600193925f926003820160021c845b81811061520257505084833510156001166151bb575b9360049184958261513696029461506c565b8192919381936f1a0111ea397fe69a4b1ba7b6434bacd781145f5160206153185f395f51905f5284148116806151b0575b156151a0575b6f1a0111ea397fe69a4b1ba7b6434bacd7905f5160206153185f395f51905f52600160801b891095111516911017161693565b600186019586109096019561516d565b5f9750879650615167565b936151369350815f5160206153185f395f51905f526f1a0111ea397fe69a4b1ba7b6434bacd76151f18460048181988c8b61506c565b929092149114169450915093615124565b828160021b8503600490818110615240575b50026101008110615229575b5060010161510e565b9760018092991b8960051b87013510169790615220565b90505f615214565b9290915f926001946003830160021c5f5b818110615271575050925f926004926151369561506c565b838160021b86036004908181106152af575b50026101008110615298575b50600101615259565b9760018092991b8960051b8501351016979061528f565b90505f61528356fe4e5280109d8f96b8bfb543a6b1af25fb56a9db616af85a90eedc558e3eb1ea2973eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff000000014997c5aa3a5fa07bcaf880a9054bef831effbd9cd58e46d9bb4fb88ef99de0db64774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff000000004382d0938a760120dd6cef8f3b90a0c38abae475e3d21e39365472b76d780272 diff --git a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.sol b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.sol new file mode 100644 index 000000000..64ba62d74 --- /dev/null +++ b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.sol @@ -0,0 +1,3572 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.24; + +/// @title Halo2 BLS12-381 KZG verifier. +/// @notice Circuit-specialized verifier for Midfall/midnight-proofs Halo2 +/// proofs rendered by this repository's Rust generator. +/// @dev This contract ports the verifier flow from +/// `midfall/proofs/src/plonk/verifier.rs`, the Keccak transcript comments from +/// `midfall/proofs/src/transcript/implementors.rs`, and the KZG multi-open +/// comments from `midfall/proofs/src/poly/kzg/mod.rs`. +/// @dev It is not a generic verifier. The proof layout, VK payload, quotient +/// identity program, memory layout, and optional quotient evaluator are all +/// generated for one `VerifyingKey>`. +/// +/// Halo2 KZG verifier for the BLS12-381 curve, midnight-proofs flavour. +/// +/// Differences vs the original BN254 / halo2 v0.4 template: +// +/// - BLS12-381 base field Fp is 381 bits and does not fit in a uint256. +/// Each Fp coord is encoded EIP-2537 padded (16 zero bytes + 48 bytes). +/// A G1 point is 128 bytes (4 words); a G2 point is 256 bytes (8). +/// - Calldata carries G1 commitments in uncompressed EIP-2537 padded +/// form (4 words = 128 bytes per point: x_hi, x_lo, y_hi, y_lo). The +/// proof bytes produced by midnight-proofs prover are repacked off +/// chain (compressed -> uncompressed) before being passed to +/// `verifyProof`. The verifier hashes the uncompressed 128-byte form into +/// the transcript verbatim, matching `Hashable for G1Projective::to_input`; +/// see `common_uncompressed_g1`. +/// - Transcript `common` absorbs raw inputs in order. `squeeze` computes one +/// Keccak digest, resets the transcript buffer to that digest, then samples +/// by interpreting the digest as a big-endian integer modulo r. +/// - Scalar inversion uses modexp(scalar, r-2, r). +/// - Constructors run a deployment-time smoke test for the EIP-2537 +/// precompiles using identity inputs. Compile with Solidity >=0.8.24 and +/// deploy only on chains/forks that support MCOPY and EIP-2537. +contract Halo2Verifier { + + /// @notice Verifying-key contract address authorized for this verifier. + /// @dev The runtime length and codehash are pinned by generated constants and checked at construction time. + address public immutable AUTHORIZED_VK; + // Expected VK runtime metadata. The deployed VK runtime is + // INVALID || payload, hence EXPECTED_VK_LENGTH is one byte longer than + // EXPECTED_VK_PAYLOAD_LENGTH. + uint256 internal constant EXPECTED_VK_PAYLOAD_LENGTH = 17024; + uint256 internal constant EXPECTED_VK_LENGTH = 17025; + uint256 internal constant EXPECTED_VK_CODEHASH_WORD = 0x24430c63ec2017f12ec4004d66c66314f418dd77fe38dd4c2475c8f2b259e009; + bytes32 internal constant EXPECTED_VK_CODEHASH = bytes32(EXPECTED_VK_CODEHASH_WORD); + + // Solidity ABI calldata cursors. The generated verifier accepts exactly + // verifyProof(bytes proof, uint256[] instances), then parses the `proof` + // bytes itself in the same order as the Rust verifier transcript. + uint256 internal constant PROOF_LEN_CPTR = 0x44; + uint256 internal constant PROOF_CPTR = 0x64; + uint256 internal constant NUM_INSTANCE_CPTR = 0x1ec4; + uint256 internal constant INSTANCE_CPTR = 0x1ee4; + // First general-purpose memory words reserved by the generated verifier. + // RETURN_MPTR is a single word set to 1 on success. + uint256 internal constant TRANSCRIPT_MPTR = 0x80; + uint256 internal constant RETURN_MPTR = 0x80; + + // ---------------------------------------------------------------------- + // Verifying-key memory map. The VK header lives at VK_MPTR, followed + // by the quotient VM payload and commitments. After the full VK + // runtime comes the challenge slots (challenge_mptr..) and the + // per-stage scratch (theta_mptr..). + // ---------------------------------------------------------------------- + uint256 internal constant VK_MPTR = 0x2700; + uint256 internal constant VK_DIGEST_MPTR = 0x2700; + uint256 internal constant NUM_INSTANCES_MPTR = 0x2720; + uint256 internal constant K_MPTR = 0x2740; + uint256 internal constant N_INV_MPTR = 0x2760; + uint256 internal constant OMEGA_MPTR = 0x2780; + uint256 internal constant OMEGA_INV_MPTR = 0x27a0; + uint256 internal constant OMEGA_INV_TO_L_MPTR = 0x27c0; + uint256 internal constant HAS_ACCUMULATOR_MPTR = 0x27e0; + uint256 internal constant ACC_OFFSET_MPTR = 0x2800; + uint256 internal constant NUM_ACC_LIMBS_MPTR = 0x2820; + uint256 internal constant NUM_ACC_LIMB_BITS_MPTR = 0x2840; + uint256 internal constant G1_BASE_MPTR = 0x2860; + uint256 internal constant G2_BASE_MPTR = 0x28e0; + uint256 internal constant NEG_S_G2_BASE_MPTR = 0x29e0; + + uint256 internal constant CHALLENGE_MPTR = 0x6980; + + // Challenge layout. Squeeze order in midnight-proofs: + // user_phase challenges (variable count) + // theta -> beta, gamma -> trash_challenge -> y -> x -> + // x1, x2 -> x3 -> x4 + uint256 internal constant THETA_MPTR = 0x6980; + uint256 internal constant BETA_MPTR = 0x69a0; + uint256 internal constant GAMMA_MPTR = 0x69c0; + uint256 internal constant TRASH_CHALLENGE_MPTR = 0x69e0; + uint256 internal constant Y_MPTR = 0x6a00; + uint256 internal constant X_MPTR = 0x6a20; + uint256 internal constant X1_MPTR = 0x6a40; + uint256 internal constant X2_MPTR = 0x6a60; + uint256 internal constant X3_MPTR = 0x6a80; + uint256 internal constant X4_MPTR = 0x6aa0; + + // Batch-open commitments live in 4-word EIP-2537 padded slots. + uint256 internal constant F_COM_MPTR = 0x6ac0; + uint256 internal constant PI_MPTR = 0x6b40; + + // Accumulator (KZG IVC). + uint256 internal constant ACC_LHS_MPTR = 0x6bc0; + uint256 internal constant ACC_RHS_MPTR = 0x6c40; + + // Lagrange / linearization scratch. + uint256 internal constant X_N_MPTR = 0x6cc0; + uint256 internal constant X_N_MINUS_1_INV_MPTR = 0x6ce0; + uint256 internal constant L_LAST_MPTR = 0x6d00; + uint256 internal constant L_BLIND_MPTR = 0x6d20; + uint256 internal constant L_0_MPTR = 0x6d40; + uint256 internal constant INSTANCE_EVAL_MPTR = 0x6d60; + // Legacy name: this is not h(x). It stores the expected opening + // scalar for the linearized commitment, i.e. the negated y-batched + // identity numerator reconstructed from the alleged evals at x. + uint256 internal constant QUOTIENT_EVAL_MPTR = 0x6d80; + uint256 internal constant QUOTIENT_MPTR = 0x6da0; // 4 words + uint256 internal constant F_EVAL_MPTR = 0x6e40; + uint256 internal constant V_MPTR = 0x6e60; + uint256 internal constant FINAL_COM_MPTR = 0x6e80; // 4 words + uint256 internal constant PAIRING_LHS_MPTR = 0x6f00; // 4 words + uint256 internal constant PAIRING_RHS_MPTR = 0x6f80; // 4 words + + // Multi-prepare scratch (sized at codegen time). + uint256 internal constant ROT_POINTS_MPTR = 0x7000; + uint256 internal constant X1_POWERS_MPTR = 0x7380; + // Q_COM materialization is currently fused into the final MSM scratch, + // so this marker intentionally aliases Q_EVAL_SET_MPTR and has zero + // reserved capacity until a future emitter starts writing Q_COM_MPTR. + uint256 internal constant Q_COM_MPTR = 0x7ba0; + uint256 internal constant Q_EVAL_SET_MPTR = 0x7ba0; + + // Q_EVAL_CPTR is set at runtime once the verifier reaches the q_evals + // block of the proof; we keep it as a memory slot for symmetry. + uint256 internal constant Q_EVAL_CPTR_MPTR = 0x82a0; + + // Reserved 4-word slot for the G1 identity (point at infinity) in + // EIP-2537 padded form. EVM memory is zero-initialised, and we + // never write to this region, so the four `mload`s below produce + // 0,0,0,0 which is exactly the identity encoding the EIP-2537 + // ec_add / ec_mul precompiles accept. + uint256 internal constant G1_IDENTITY_MPTR = 0x83a0; + + // Decoded polynomial-eval buffer (Optimisation H3). The off-chain + // Solidity proof shim rewrites proof scalars into canonical BE words, + // so `calldataload` gives the field element directly. The transcript- + // side `evaluations` loop range-checks and spills that value here so + // downstream eval references (gate evaluator + PCS q_eval Horner) + // become 3-gas `mload(...)` instead of calldata reads. + uint256 internal constant REVERSED_EVALS_MPTR = 0x8500; + uint256 internal constant SELECTOR_ACC_MPTR = 0xa1c0; + uint256 internal constant QUOTIENT_RETURN_MPTR = 0x80; + uint256 internal constant BATCH_INV_SCRATCH_MPTR = 0xa1c0; + uint256 internal constant TRACE_U256_MPTR = 0xd3c0; + + // ---------------------------------------------------------------------- + // Per-category bases for EIP-2537 padded G1 commitments. The proof + // calldata carries 128-byte uncompressed/padded G1s after the off-chain + // proof shim repacks midnight-proofs' native compressed stream; this + // region stores the 4-word slots used by PCS / quotient-fold sections. + // + // Cumulative offsets (in words from `comms_mptr_base`): + // ADVICE_COMMS_MPTR_BASE + 0 + // LOOKUP_M_COMMS_MPTR_BASE + 4*total_advices + // PERM_Z_COMMS_MPTR_BASE + 4*total_advices + 4*num_lookups + // LOOKUP_HELPER_COMMS_MPTR_BASE + ... + 4*num_permutation_zs + // LOOKUP_Z_COMMS_MPTR_BASE + ... + 4*lookup_helper_chunks_total + // TRASHCAN_COMMS_MPTR_BASE + ... + 4*num_lookups + // QUOTIENT_LIMB_COMMS_MPTR_BASE + ... + 4*num_trashcans + // ---------------------------------------------------------------------- + uint256 internal constant ADVICE_COMMS_MPTR_BASE = 0x91c0; + uint256 internal constant LOOKUP_M_COMMS_MPTR_BASE = 0x9940; + uint256 internal constant PERM_Z_COMMS_MPTR_BASE = 0x9a40; + uint256 internal constant LOOKUP_HELPER_COMMS_MPTR_BASE = 0x9d40; + uint256 internal constant LOOKUP_Z_COMMS_MPTR_BASE = 0x9e40; + uint256 internal constant TRASHCAN_COMMS_MPTR_BASE = 0x9f40; + uint256 internal constant QUOTIENT_LIMB_COMMS_MPTR_BASE = 0x9fc0; + + // BLS12-381 scalar-field modulus, used for transcript challenges and all + // Halo2 verifier arithmetic. + uint256 internal constant FR_MODULUS = 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001; + + // BLS12-381 Fp modulus minus one, split like an EIP-2537 coordinate: + // high word = 16 zero bytes || top 16 coordinate bytes, low word = + // bottom 32 coordinate bytes. + uint256 internal constant BLS_P_HI = 0x000000000000000000000000000000001a0111ea397fe69a4b1ba7b6434bacd7; + uint256 internal constant BLS_P_MINUS_ONE_LO = 0x64774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa; + // Packed public-accumulator sentinels for the shifted coordinate codec. + // The `_WITH_ID_FLAG` variant is used only for the first x-coordinate word. + uint256 internal constant BLS_P_MINUS_ONE_PACKED_0 = 0x00000000f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa; + uint256 internal constant BLS_P_MINUS_ONE_PACKED_0_WITH_ID_FLAG = 0x00000000f38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa; + uint256 internal constant BLS_P_MINUS_ONE_PACKED_1 = 0x0000000000000000000000001a0111ea397fe69a4b1ba7b6434bacd764774b84; + + /// @notice Smoke-check the BLS12-381 precompiles required by the verifier. + /// @dev Uses identity inputs to catch absent EIP-2537 implementations, short return data, and incompatible pairing semantics at deployment. + function require_eip2537_precompiles() private view { + assembly ("memory-safe") { + // Scratch is reused for every precompile probe. Start with the + // EIP-2537 identity encoding for G1/G2: all-zero padded words. + let scratch := 0x80 + for { let off := 0 } lt(off, 0x0180) { off := add(off, 0x20) } { + mstore(add(scratch, off), 0) + } + + // G1ADD(identity, identity) -> identity, 128-byte return. + // This catches chains where the precompile is missing or returns a + // non-standard success shape. + if iszero(staticcall(50000, 0x0b, scratch, 0x0100, scratch, 0x80)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } + if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { + revert(0, 0) + } + + // G1MSM([(identity, 0)]) -> identity, 128-byte return. + // The production verifier uses G1MSM both for commitments and as + // the subgroup validator for absorbed proof points. + if iszero(staticcall(60000, 0x0c, scratch, 0xa0, scratch, 0x80)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } + if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { + revert(0, 0) + } + + // PAIRING_CHECK([(identity_g1, identity_g2)]) -> true, + // 32-byte return. This catches absent pairing precompiles, + // short return data, and obviously incompatible semantics. + if iszero(staticcall(120000, 0x0f, scratch, 0x0180, scratch, 0x20)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + if iszero(eq(mload(scratch), 1)) { revert(0, 0) } + } + } + + + /// @notice Create a verifier pinned to a generated verifying key. + /// @dev Checks EIP-2537 availability and verifies the VK runtime before storing its address. + /// @param authorizedVk Address of the generated `Halo2VerifyingKey` runtime. + constructor(address authorizedVk) { + // Embedded quotient path: only the external VK runtime needs to be + // pinned, but the curve precompiles are still mandatory. + require_eip2537_precompiles(); + require( + authorizedVk.code.length == EXPECTED_VK_LENGTH + && authorizedVk.codehash == EXPECTED_VK_CODEHASH, + "invalid vk" + ); + AUTHORIZED_VK = authorizedVk; + } + + /// @notice Verify a Halo2/Midfall proof for the generated verifying key. + /// @dev This checks only that `proof` verifies for the supplied public + /// `instances` under this pinned VK/protocol. Application contracts must + /// bind the meaning of those instances separately: state roots, program + /// identifiers, expected IVC outputs, chain/domain separation, and any + /// protocol-specific authorization are outside this raw verifier ABI. + /// @dev Production renders are success-or-revert: accepted proofs return + /// `true`, while malformed calldata, invalid proof material, failed + /// precompiles, or mismatched pinned dependency code revert. Trace and gas + /// renders keep the same failure policy. + /// @dev The generated verifier uses absolute Yul memory addresses instead + /// of Solidity's free-memory pointer, but generated scratch starts at + /// `0x80` so Solidity's reserved memory prefix is preserved. The main + /// assembly block remains terminal: accepted proofs return from assembly + /// and all rejected inputs revert. Do not inline this body into Solidity + /// code that continues executing after verification without reviewing the + /// memory strategy; see `docs/MEMORY_LAYOUT.md`. + /// @param proof Solidity-facing proof bytes, with G1 elements repacked into EIP-2537 padded uncompressed form. + /// @param instances Public instance scalars encoded as canonical BLS12-381 scalar-field words. + /// @return Always `true` for accepted proofs; invalid proofs revert instead of returning `false`. + function verifyProof( + bytes calldata proof, + uint256[] calldata instances + ) external returns (bool) { + // Cheap ABI-shape guard before any generated memory work: + // - proof head must point at the bytes payload; + // - instances head must point at the generated instance array. + // + // The verifier below is a hand-rolled calldata parser. Failing here + // keeps malformed dynamic-argument layouts from being interpreted as a + // valid Midfall proof stream. + assembly ("memory-safe") { + if iszero(and(eq(calldataload(0x04), 0x40), eq(calldataload(0x24), sub(NUM_INSTANCE_CPTR, 0x04)))) { + revert(0, 0) + } + } + // Non-embedded renders pin the VK by address and codehash. The Yul + // loader rechecks the runtime before every proof and copies the + // INVALID-prefixed payload into VK_MPTR. + address vk = AUTHORIZED_VK; + assembly ("memory-safe") { + // This block owns the call-frame memory and remains terminal. + // Generated scratch starts at TRANSCRIPT_MPTR (0x80), preserving + // Solidity's reserved scratch, free-memory-pointer, and zero-slot + // words. See docs/MEMORY_LAYOUT.md. + // =============================================================== + // Helpers: modexp, transcript, EIP-2537 calls + // =============================================================== + + // Inverse of a Fr scalar via modexp(x, r-2, r). The verifier + // calls this only after transcript absorption is complete, so it + // reuses the dead transcript buffer just below VK_MPTR instead of + // a fixed post-VK address that can collide with live PCS scratch + // when the VK payload becomes smaller. + function scalar_inv(x) -> inv { + // Zero has no multiplicative inverse in Fr; callers rely on a + // revert here rather than a bogus modexp result. + if iszero(x) { revert(0, 0) } + let p := 0x2600 + // EIP-198 modexp frame: + // [base_len, exp_len, mod_len, base, exponent, modulus] + mstore(add(p, 0x00), 0x20) // base len + mstore(add(p, 0x20), 0x20) // exp len + mstore(add(p, 0x40), 0x20) // mod len + mstore(add(p, 0x60), x) + mstore(add(p, 0x80), sub(FR_MODULUS, 2)) + mstore(add(p, 0xa0), FR_MODULUS) + if iszero(staticcall(gas(), 0x05, p, 0xc0, p, 0x20)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + inv := mload(p) + } + + // ---------- Streaming Keccak256 transcript helpers ---------- + // + // The transcript buffer lives at + // memory[TRANSCRIPT_MPTR..buf_len). On verifier entry it starts + // empty. Each common(input) appends raw bytes. squeeze_*(buf_len) + // computes one Keccak digest, reseeds the buffer with that + // 32-byte digest, and samples a Fq element as + // uint256(digest_be) mod r. + + function transcript_init() -> buf_len { + // Empty transcript buffer starts exactly at TRANSCRIPT_MPTR. + buf_len := TRANSCRIPT_MPTR + } + + // Append one 32-byte big-endian field/transcript word at the + // current end of the transcript buffer. + function common_word(buf_len, word) -> ret { + mstore(buf_len, word) + ret := add(buf_len, 32) + } + + // Absorb a BLS12-381 G1 point in EIP-2537 padded + // uncompressed form (4 calldata words = 128 bytes: + // x_hi || x_lo || y_hi || y_lo, each coord = 16 zero + // pad bytes + 48 big-endian field bytes) into the + // transcript buffer at `buf_len`. + // + // Matches the patched `Hashable for + // midnight_curves::G1Projective::to_input` in + // midnight-proofs, which now emits the same 128-byte form + // (`midfall/proofs/src/transcript/implementors.rs`). The + // previous emitter hashed the 48-byte ZCash compressed + // encoding instead and ran a 384-bit `lex(y) > lex(p − y)` + // ladder + identity flag fixup to derive the sign bit on + // the fly; switching to the uncompressed form drops that + // ladder entirely. + // + // Canonicality: reject non-zero bytes in the top 16 bytes + // of each `_hi` calldata word and reject coordinates + // outside Fp. Normalizing those bytes before hashing would + // make multiple calldata encodings share one transcript. + // + // This helper does not run an independent curve/subgroup + // check. Instead, ProtocolPlan::validate rejects generated + // plans where an absorbed proof commitment would not later be + // consumed by an EIP-2537 G1MSM or pairing path, and those + // precompiles perform the curve/subgroup validation. + // + // The point's uncompressed form remains in calldata; the + // call site is responsible for `calldatacopy`-ing it into + // memory afterwards if it needs the on-curve coordinates. + function common_uncompressed_g1(buf_len, cptr) -> ret { + let x_hi_word := calldataload(cptr) + let x_lo := calldataload(add(cptr, 0x20)) + let y_hi_word := calldataload(add(cptr, 0x40)) + let y_lo := calldataload(add(cptr, 0x60)) + if shr(128, x_hi_word) { revert(0, 0) } + if shr(128, y_hi_word) { revert(0, 0) } + + let x_hi := and(x_hi_word, 0xffffffffffffffffffffffffffffffff) + let y_hi := and(y_hi_word, 0xffffffffffffffffffffffffffffffff) + if iszero(or(lt(x_hi, BLS_P_HI), and(eq(x_hi, BLS_P_HI), iszero(gt(x_lo, BLS_P_MINUS_ONE_LO))))) { + revert(0, 0) + } + if iszero(or(lt(y_hi, BLS_P_HI), and(eq(y_hi, BLS_P_HI), iszero(gt(y_lo, BLS_P_MINUS_ONE_LO))))) { + revert(0, 0) + } + + // Memcpy the 4 calldata words (128 bytes) verbatim + // into the keccak buffer. + calldatacopy(buf_len, cptr, 0x80) + ret := add(buf_len, 0x80) + } + + // One Keccak finalization + reseed. Returns the new buffer + // cursor (= TRANSCRIPT_MPTR + 32) and stores the squeezed Fq at + // `mptr`. + function squeeze_to(buf_len, mptr) -> ret { + let h0 := keccak256(TRANSCRIPT_MPTR, sub(buf_len, TRANSCRIPT_MPTR)) + // Reseed: write the 32-byte digest at start of buffer. + mstore(TRANSCRIPT_MPTR, h0) + let r := FR_MODULUS + // Sample Fq as uint256(keccak_digest_be) mod r. + mstore(mptr, mod(h0, r)) + ret := add(TRANSCRIPT_MPTR, 32) + } + + // ---------- EC primitives (EIP-2537 wrappers) ---------- + // + // These mirror the BN254 helpers but operate on 4-word G1 + // points. They use planned memory windows above Solidity's + // reserved prefix; the streaming transcript buffer is no longer + // needed once all challenges are squeezed. + + // Invert a contiguous run of Fr words in-place using Montgomery's + // batch inversion trick: + // 1. write prefix products to scratch; + // 2. invert the total product once with modexp; + // 3. walk backward to recover each individual inverse. + // + // The function returns a boolean instead of reverting so callers + // can combine it with other `success` plumbing until a section + // boundary decides whether to fail closed. + function batch_invert(success, mptr_start, mptr_end, scratch_mptr, r) -> ret { + ret := success + if iszero(ret) { leave } + // Memory ranges must be forward and word-aligned by + // construction; a reversed range is always a codegen error. + if lt(mptr_end, mptr_start) { + ret := 0 + leave + } + + let count_bytes := sub(mptr_end, mptr_start) + // Empty batch is valid and leaves memory untouched. + if iszero(count_bytes) { leave } + + // Fast path for a single denominator: avoid prefix scratch and + // just run one modexp inverse in place. + if eq(count_bytes, 0x20) { + let x := mload(mptr_start) + if iszero(x) { + ret := 0 + leave + } + + let single_scratch := scratch_mptr + mstore(add(single_scratch, 0x00), 0x20) + mstore(add(single_scratch, 0x20), 0x20) + mstore(add(single_scratch, 0x40), 0x20) + mstore(add(single_scratch, 0x60), x) + mstore(add(single_scratch, 0x80), sub(r, 2)) + mstore(add(single_scratch, 0xa0), r) + ret := staticcall(gas(), 0x05, single_scratch, 0xc0, single_scratch, 0x20) + ret := and(ret, eq(returndatasize(), 0x20)) + if ret { mstore(mptr_start, mload(single_scratch)) } + leave + } + + // Forward pass: scratch stores prefix products up to, but not + // including, the final element. `gp` becomes the total product. + let gp_mptr := scratch_mptr + let gp := mload(mptr_start) + let mptr := add(mptr_start, 0x20) + for {} lt(mptr, sub(mptr_end, 0x20)) {} { + gp := mulmod(gp, mload(mptr), r) + mstore(gp_mptr, gp) + mptr := add(mptr, 0x20) + gp_mptr := add(gp_mptr, 0x20) + } + gp := mulmod(gp, mload(mptr), r) + // A zero total product means at least one denominator was + // zero, so no batch inverse exists. + if iszero(gp) { + ret := 0 + leave + } + + // Invert the total product once. + mstore(add(gp_mptr, 0x00), 0x20) + mstore(add(gp_mptr, 0x20), 0x20) + mstore(add(gp_mptr, 0x40), 0x20) + mstore(add(gp_mptr, 0x60), gp) + mstore(add(gp_mptr, 0x80), sub(r, 2)) + mstore(add(gp_mptr, 0xa0), r) + ret := staticcall(gas(), 0x05, gp_mptr, 0xc0, gp_mptr, 0x20) + ret := and(ret, eq(returndatasize(), 0x20)) + let all_inv := mload(gp_mptr) + + // Backward pass: derive each inverse from the inverted total + // product and the saved prefix products. + let first_mptr := mptr_start + let second_mptr := add(first_mptr, 0x20) + gp_mptr := sub(gp_mptr, 0x20) + for {} lt(second_mptr, mptr) {} { + let inv := mulmod(all_inv, mload(gp_mptr), r) + all_inv := mulmod(all_inv, mload(mptr), r) + mstore(mptr, inv) + mptr := sub(mptr, 0x20) + gp_mptr := sub(gp_mptr, 0x20) + } + let inv_first := mulmod(all_inv, mload(second_mptr), r) + let inv_second := mulmod(all_inv, mload(first_mptr), r) + mstore(first_mptr, inv_first) + mstore(second_mptr, inv_second) + } + + // Final EIP-2537 pairing wrapper. `lhs_mptr` and `rhs_mptr` are + // 4-word G1 slots; G2 bases are loaded from the pinned VK payload. + function ec_pairing(success, lhs_mptr, rhs_mptr) -> ret { + ret := success + if iszero(ret) { leave } + // Lay out two (G1, G2) pairs at scratch..scratch+0x300: + // [lhs_g1 (0x80) | G2_BASE (0x100) | rhs_g1 (0x80) | NEG_S_G2_BASE (0x100)] + // Cancun MCOPY (3 + 3·words gas) replaces what used to + // be a 4-step mstore chain for each G1 (~60 gas) and an + // 8-iter mstore loop for each G2 (~240 gas). Net saving + // here is ~500 gas per ec_pairing call. + let scratch := 0x0300 + mcopy(scratch, lhs_mptr, 0x80) + mcopy(add(scratch, 0x80), G2_BASE_MPTR, 0x100) + mcopy(add(scratch, 0x180), rhs_mptr, 0x80) + mcopy(add(scratch, 0x200), NEG_S_G2_BASE_MPTR, 0x100) + ret := staticcall(170000, 0x0f, scratch, 0x0300, scratch, 0x20) + ret := and(ret, eq(returndatasize(), 0x20)) + ret := and(ret, mload(scratch)) + if iszero(ret) { revert(0, 0) } + ret := 1 + } + + // ---------- IVC accumulator public-input decoding ---------- + // + // `AssignedForeignPoint` exposes each base-field coordinate + // through `AssignedField::as_public_input`: seven radix-2^56 limbs of + // (coord - 1) are packed four-at-a-time into native field elements. + // The x coordinate's first packed word carries the identity flag by + // adding one raw radix base. Rebuild EIP-2537 padded + // (x_hi, x_lo, y_hi, y_lo) words from that encoding. + // + // Public-input layout for one coordinate: + // word 0: limb_0 | limb_1 << bits | ... up to limbs_per_word + // word 1: next limbs, if any + // + // The limbs are little-endian in the represented integer even + // though calldata words are loaded as big 256-bit values. The loop + // below extracts each limb by shifting inside the packed word and + // reconstructs the full coordinate into the two-word EIP-2537 + // representation expected by the BLS12-381 precompiles. + function load_acc_coord_shifted(src, bits, n, base, limbs_per_word, first_adjust) -> hi, lo { + // Mask for one radix limb, e.g. 2^56 - 1 for the current + // BLS12-381 self-emulation parameters. + let mask := sub(base, 1) + for { let i := 0 } lt(i, n) { i := add(i, 1) } { + // Limb words are little-endian packed inside each Fr + // public input. `first_adjust` removes the identity flag + // base from the first x word when present. + let packed := calldataload(add(src, mul(div(i, limbs_per_word), 0x20))) + if and(iszero(div(i, limbs_per_word)), first_adjust) { + packed := sub(packed, first_adjust) + } + // Select limb i from its packed field word. The mod/div + // pair maps a limb index to an intra-word limb slot and + // the calldata word containing it. + let limb := and(shr(mul(mod(i, limbs_per_word), bits), packed), mask) + + let shift := mul(i, bits) + // Split the reconstructed 384-bit coordinate into the + // EIP-2537 high/low words expected by the precompiles. + if lt(shift, 256) { + lo := add(lo, shl(shift, limb)) + if gt(add(shift, bits), 256) { + // A limb can straddle the 256-bit low/high split. + // Move the overflow bits into hi. + hi := add(hi, shr(sub(256, shift), limb)) + } + } + if iszero(lt(shift, 256)) { + // Once shift >= 256 the whole limb belongs to hi. + hi := add(hi, shl(sub(shift, 256), limb)) + } + } + } + + // The shifted coordinate codec represents zero as p-1 before the + // final +1 below, so keep this sentinel explicit. + function is_bls_p_minus_one(hi, lo) -> yes { + yes := and(eq(hi, BLS_P_HI), eq(lo, BLS_P_MINUS_ONE_LO)) + } + + // Canonical encoded accumulator identity: + // x = p-1 plus the identity flag in the first packed word, + // y = p-1 with no identity flag. + // It decodes to the EIP-2537 point-at-infinity slot (all zeros). + // + // This fast path is deliberately stricter than "decodes to zero": + // the point at infinity has exactly one accepted public-input + // encoding. Non-canonical zero-like encodings are rejected later. + function is_acc_encoded_identity(src) -> yes { + yes := and( + and( + eq(calldataload(src), BLS_P_MINUS_ONE_PACKED_0_WITH_ID_FLAG), + eq(calldataload(add(src, 0x20)), BLS_P_MINUS_ONE_PACKED_1) + ), + and( + eq(calldataload(add(src, 0x40)), BLS_P_MINUS_ONE_PACKED_0), + eq(calldataload(add(src, 0x60)), BLS_P_MINUS_ONE_PACKED_1) + ) + ) + } + + // Reject unused high bits in the packed public-input words. This + // makes each accumulator point encoding canonical before it reaches + // the precompile-based curve/subgroup validation. + function check_acc_coord_packing(src, bits, n, limbs_per_word) -> ok { + ok := 1 + // Number of packed native-field public-input words occupied by + // one coordinate. + let coord_words := div(add(n, sub(limbs_per_word, 1)), limbs_per_word) + for { let word_idx := 0 } lt(word_idx, coord_words) { word_idx := add(word_idx, 1) } { + // The final word may contain fewer than limbs_per_word + // limbs. Any unused high bits must be zero, otherwise the + // same coordinate would have multiple calldata encodings. + let remaining := sub(n, mul(word_idx, limbs_per_word)) + let limbs_in_word := limbs_per_word + if lt(remaining, limbs_per_word) { + limbs_in_word := remaining + } + let used_bits := mul(limbs_in_word, bits) + if lt(used_bits, 256) { + // shl(used_bits, 1) == 2^used_bits. The packed word + // must be strictly less than that bound. + ok := and(ok, lt(calldataload(add(src, mul(word_idx, 0x20))), shl(used_bits, 1))) + } + } + } + + // Decode one shifted coordinate. `allow_id` is true only for x, + // because the identity flag lives in x's first packed word. + function load_acc_coord(src, allow_id, bits, n, base, limbs_per_word) -> ok, hi, lo, is_id { + ok := check_acc_coord_packing(src, bits, n, limbs_per_word) + if and(allow_id, iszero(lt(calldataload(src), base))) { + // Probe the x identity flag by removing one radix base and + // checking whether the adjusted coordinate is p-1. + // + // `calldataload(src) >= base` is a cheap prefilter: only x + // can carry this flag, and adding one radix base must make + // the first packed word at least base. + let adj_hi, adj_lo := load_acc_coord_shifted(src, bits, n, base, limbs_per_word, base) + is_id := is_bls_p_minus_one(adj_hi, adj_lo) + } + + // Decode again with the identity adjustment applied only when + // the canonical identity flag was actually detected. + hi, lo := load_acc_coord_shifted(src, bits, n, base, limbs_per_word, mul(is_id, base)) + ok := and( + ok, + // Coordinate must be in the BLS12-381 base field, i.e. + // <= p - 1 in split hi/lo form. + or(lt(hi, BLS_P_HI), and(eq(hi, BLS_P_HI), iszero(gt(lo, BLS_P_MINUS_ONE_LO)))) + ) + + let was_p_minus_one := is_bls_p_minus_one(hi, lo) + if was_p_minus_one { + // Shifted encoding maps p-1 back to zero. + hi := 0 + lo := 0 + } + if iszero(was_p_minus_one) { + // All other coordinates are encoded as coord - 1, so add + // one back with carry into the high word. + let next_lo := add(lo, 1) + hi := add(hi, lt(next_lo, lo)) + lo := next_lo + } + + // EIP-2537 pads each 48-byte Fp coordinate to 64 bytes, + // so the high word must fit in its low 128 bits. + // This also catches impossible reconstructions above 384 bits. + ok := and(ok, lt(hi, shl(128, 1))) + } + + // Decode a public accumulator point into an EIP-2537 4-word G1 + // slot. Non-identity points are curve/subgroup checked later by + // routing them through G1MSM. + function load_acc_point(dst, src, bits, n, base) -> ok, is_id { + // Prefer the canonical all-coordinate identity encoding before + // attempting coordinate-level shifted decoding. This accepts + // the point at infinity only in the exact form generated by the + // circuit's public-input codec. + is_id := is_acc_encoded_identity(src) + if is_id { + ok := 1 + // EIP-2537 encodes G1 identity as four zero words: + // x_hi = x_lo = y_hi = y_lo = 0. + mstore(dst, 0) + mstore(add(dst, 0x20), 0) + mstore(add(dst, 0x40), 0) + mstore(add(dst, 0x60), 0) + } + if iszero(is_id) { + // x occupies coord_words packed public-input words; y + // starts immediately after x. + let limbs_per_word := 4 + let coord_words := div(add(n, sub(limbs_per_word, 1)), limbs_per_word) + // Only x may carry the identity flag. y must decode as a + // normal shifted coordinate. + let x_ok, x_hi, x_lo, x_is_id := load_acc_coord(src, 1, bits, n, base, limbs_per_word) + let y_ok, y_hi, y_lo, y_id := load_acc_coord( + add(src, mul(coord_words, 0x20)), + 0, + bits, + n, + base, + limbs_per_word + ) + // y_id is always zero because allow_id was false, but the + // tuple shape is shared with x decoding. + pop(y_id) + ok := and(x_ok, y_ok) + is_id := x_is_id + + if is_id { + // If x carried the identity flag, both decoded + // coordinates must be zero after shifting. Any other y + // value would be a malformed infinity encoding. + ok := and(ok, iszero(or(or(x_hi, x_lo), or(y_hi, y_lo)))) + mstore(dst, 0) + mstore(add(dst, 0x20), 0) + mstore(add(dst, 0x40), 0) + mstore(add(dst, 0x60), 0) + } + if iszero(is_id) { + // The coordinate codec maps encoded p-1 to decoded + // zero. EIP-2537 reserves affine (0,0) for the point + // at infinity, so a decoded infinity is only valid + // when the canonical accumulator identity encoding + // was used above. + let decoded_zero := iszero(or(or(x_hi, x_lo), or(y_hi, y_lo))) + ok := and(ok, iszero(decoded_zero)) + // Store the affine point in the exact precompile input + // layout: x_hi, x_lo, y_hi, y_lo. + mstore(dst, x_hi) + mstore(add(dst, 0x20), x_lo) + mstore(add(dst, 0x40), y_hi) + mstore(add(dst, 0x60), y_lo) + } + } + } + // Validate and prepare the public accumulator equation before the + // main transcript starts. This fails malformed public inputs early + // and writes ACC_LHS_MPTR / ACC_RHS_MPTR for final pairing batching. + // + // The accumulator public input represents an equality of two G1 + // commitments used by the recursive KZG accumulator. This helper: + // 1. decodes carried public G1 points from shifted limbs; + // 2. forces every decoded point through EIP-2537 G1MSM so the + // precompile validates curve/subgroup membership; + // 3. folds the RHS carried point and fixed-base scalar tail into + // ACC_RHS_MPTR, leaving ACC_LHS_MPTR / ACC_RHS_MPTR ready for + // randomized batching in FinalPairing.yul. + function validate_public_accumulator(success, r) -> out { + out := success + let bits := 56 + let n := 7 + // The BLS12-381 self-emulation currently exposes Fp + // coordinates as 7 radix-2^56 limbs. + let limb_base := shl(bits, 1) + let limbs_per_word := 4 + let coord_words := div(add(n, sub(limbs_per_word, 1)), limbs_per_word) + // acc_offset is generated from the VK/protocol shape and + // points into the ABI `instances` array. + let acc_instance_ptr := add(INSTANCE_CPTR, 0x0160) + + // LHS layout: point limbs (x,y), then either an explicit + // scalar word or an implicit unit scalar for already-collapsed + // point-pair public inputs. + // The scalar pointer is computed unconditionally; the rendered + // branch below decides whether to read it or use scalar 1. + let lhs_scalar_ptr := add(acc_instance_ptr, mul(mul(2, coord_words), 0x20)) + let lhs_ok, lhs_is_id := load_acc_point(ACC_LHS_MPTR, acc_instance_ptr, bits, n, limb_base) + out := and(out, lhs_ok) + // Shared scratch for one-pair LHS validation and the later + // variable-length RHS MSM. + let acc_scratch := 0xa1c0 + { + // Already-collapsed point-pair layout: carried scalars are + // implicit one. + let lhs_scalar := 1 + // Identity status is useful for decoding checks above, but + // validation still goes through G1MSM for all points. + pop(lhs_is_id) + // Always route the decoded carried point through G1MSM, + // even for identity points and zero/one scalars. The + // precompile is the on-curve/subgroup validator for this + // public-input point; skipping it would let a malformed + // non-identity point hide behind scalar 0. + mcopy(acc_scratch, ACC_LHS_MPTR, 0x80) + mstore(add(acc_scratch, 0x80), lhs_scalar) + if out { + // Single-pair MSM output overwrites ACC_LHS_MPTR with + // lhs_scalar * decoded_lhs. If lhs_scalar is one, this + // is also a curve/subgroup validation round-trip. + out := staticcall(62000, 0x0c, acc_scratch, 0xa0, ACC_LHS_MPTR, 0x80) + out := and(out, eq(returndatasize(), 0x80)) + } + } + // RHS layout for this generated verifier is an already + // collapsed point pair: lhs point, rhs point. Both carried + // scalars are implicit one, and there is no fixed-base scalar + // tail. + let rhs_instance_ptr := lhs_scalar_ptr + // RHS scalar, when present, immediately follows the RHS point + // limbs. The fixed-base scalar tail starts after it. + let rhs_scalar_ptr := add(rhs_instance_ptr, mul(mul(2, coord_words), 0x20)) + let rhs_ok, rhs_is_id := load_acc_point(ACC_RHS_MPTR, rhs_instance_ptr, bits, n, limb_base) + out := and(out, rhs_ok) + // acc_pair_ptr appends (G1, scalar) pairs into acc_scratch for + // one final RHS MSM. + let acc_pair_ptr := acc_scratch + { + // Implicit unit scalar for already-collapsed point pairs. + let rhs_scalar := 1 + pop(rhs_is_id) + // Keep the carried RHS point in the MSM input even when + // it is encoded as identity or has scalar 0/1, so EIP-2537 + // validates every decoded public accumulator point before + // it can affect, or be erased from, the pairing batch. + mcopy(acc_pair_ptr, ACC_RHS_MPTR, 0x80) + mstore(add(acc_pair_ptr, 0x80), rhs_scalar) + // Move to the next (G1, scalar) pair slot. + acc_pair_ptr := add(acc_pair_ptr, 0xa0) + } + // Total byte length of the appended RHS MSM input pairs. This + // is at least one pair because the carried RHS point is always + // appended; keep the guard for synthetic render configurations. + let acc_msm_len := sub(acc_pair_ptr, acc_scratch) + if acc_msm_len { + // Fold the carried RHS point and any generated fixed-base + // tail into ACC_RHS_MPTR. The later final pairing block + // randomizes this equation together with the KZG pairing. + if out { + // Output overwrites ACC_RHS_MPTR with: + // rhs_scalar * carried_rhs + // + sum_i fixed_scalar_i * fixed_base_i + // + // The precompile also validates every nonzero fixed + // base embedded by codegen and the carried RHS point. + out := staticcall( + 62000, + 0x0c, + acc_scratch, + acc_msm_len, + ACC_RHS_MPTR, + 0x80 + ) + out := and(out, eq(returndatasize(), 0x80)) + } + } + // The caller checks `out` and reverts before transcript work if + // any decode, canonicality, or precompile validation failed. + } + + + // Section-boundary gas-attribution checkpoint. Emits a + // single LOG1 (no data) with topic = (id << 248) | gas(). + // Cost: 375 (LOG base) + 375 (1 topic) = 750 gas/call. + // Host-side parses the topic into (id, gas_left) and prints + // pairwise deltas (see `dump_gas_checkpoints`). + function gas_checkpoint(id) { + log1(0, 0, or(shl(248, id), gas())) + } + + let r := FR_MODULUS + let success := true + + + gas_checkpoint(1) // entry: before VK loading + + // =============================================================== + // VK loading: either bake in the embedded VK bytes or fetch + // them from the linked AUTHORIZED_VK contract. + // + // This is the first verifier phase after helper definitions. Its + // job is to make the generated VK payload available at VK_MPTR in + // one canonical memory layout, regardless of whether this render + // embeds the VK directly or links a separate Halo2VerifyingKey + // contract. + // + // Later template partials treat VK_MPTR as already populated with: + // - header words: vk_digest, domain data, accumulator metadata; + // - BLS12-381 base points used by the final pairing; + // - compact quotient VM constants/program bytes, when enabled; + // - fixed and permutation commitments in 4-word G1 slots. + // =============================================================== + { + // Re-check the pinned VK dependency on every proof. The + // constructor check catches normal deployment mistakes, while + // this fresh check hardens forks or same-transaction edge + // cases where code at the authorized address could differ + // from the runtime originally pinned by this verifier. + // + // EXPECTED_VK_LENGTH includes the leading INVALID byte in the + // Halo2VerifyingKey runtime. EXPECTED_VK_CODEHASH_WORD is the + // full runtime hash, not only the payload hash. + if iszero(and( + eq(extcodesize(vk), EXPECTED_VK_LENGTH), + eq(extcodehash(vk), EXPECTED_VK_CODEHASH_WORD) + )) { revert(0, 0) } + // Runtime byte 0 is INVALID so direct calls cannot execute the + // payload. Copy from byte 1 into VK_MPTR to reconstruct the + // exact payload layout used by the embedded branch. + extcodecopy(vk, VK_MPTR, 0x01, EXPECTED_VK_PAYLOAD_LENGTH) + + // This verifier is pinned to one generated VK, so schema + // values such as instance count and accumulator layout are + // rendered as constants instead of reread from the VK header. + // + // The checks below validate the dynamic ABI envelope before the + // transcript parser starts walking raw calldata: + // - proof bytes length equals the generated proof layout; + // - instance array length equals the generated public input + // count; + // - total calldata length has no missing or trailing words. + // + // `success` is folded through `and` for consistency with later + // sections, then immediately enforced at the end of this block. + // A failure here means the verifier is not looking at the proof + // shape it was generated to parse. + success := and(success, eq(0x1e60, calldataload(PROOF_LEN_CPTR))) + success := and(success, eq(19, calldataload(NUM_INSTANCE_CPTR))) + // Calldata must contain exactly the ABI selector, proof bytes, + // instance-array length, and generated number of instance + // words. Any trailing bytes fail closed. + success := and( + success, + eq(calldatasize(), add(INSTANCE_CPTR, 0x0260)) + ) + // Stop before any transcript absorption if the ABI/proof shape + // is not exactly the generated one. + if iszero(success) { revert(0, 0) } + } + // Fail malformed accumulator public inputs before transcript, + // quotient, PCS, and final pairing work. The late accumulator block + // only batches these already-validated G1 outputs into the final + // pairing equation. + // + // Accumulator validation decodes shifted public-input limbs into + // EIP-2537 G1 slots, checks canonical encodings, and routes points + // through G1MSM for curve/subgroup validation. Doing it here means + // invalid accumulator public inputs cannot influence transcript + // challenge derivation or waste gas in later quotient/PCS work. + // validate_public_accumulator returns a boolean to share the same + // success-plumbing style as other helper calls; this boundary is + // where the verifier converts failure to a revert. + success := validate_public_accumulator(success, r) + if iszero(success) { revert(0, 0) } + gas_checkpoint(2) // after VK loading + accumulator public-input precheck + + // =============================================================== + // Transcript: VK digest + instances + proof. + // + // This block is the Solidity mirror of the native Midfall verifier + // transcript schedule. It does three jobs at once: + // + // 1. Absorb public data and proof bytes into the streaming + // Keccak transcript in exactly the native order. + // 2. Decode/range-check proof scalars and canonical G1 calldata. + // 3. Copy proof commitments/evaluations into planned memory + // slots consumed by Lagrange, quotient, PCS, and pairing + // blocks later in the verifier. + // + // `buf_len` is a write cursor into the transcript buffer. The + // helper functions append bytes and return the new cursor; squeeze + // helpers hash memory[TRANSCRIPT_MPTR..buf_len), reseed the buffer + // with the digest, and write the sampled Fr challenge to memory. + // =============================================================== + let buf_len := transcript_init() + // VK_DIGEST_MPTR holds the digest as a BE 32-byte word (the + // VK contract stores it via `mstore`, which matches the + // Keccak Fq transcript input). + // + // This digest commits to the verifier key / constraint system + // before any proof material is read. + buf_len := common_word(buf_len, mload(VK_DIGEST_MPTR)) + + // Absorb committed_pi = G1Affine::identity() when the + // `committed-instances` feature is on in midnight-proofs. + // Under the patched `Hashable::to_input` (see + // `midfall/proofs/src/transcript/implementors.rs`), the + // identity hashes as 128 zero bytes (EIP-2537 (0,0) + // convention), NOT the 48-byte ZCash compressed form + // 0xc0||47*0x00 that the previous emitter produced. + // Native verifier absorbs this BEFORE the instance count. + { + // 128 zero bytes: zero out 4 consecutive 32-byte words + // at buf_len. + // This is a raw transcript absorb, not a memory slot kept for + // later elliptic-curve operations. + mstore(buf_len, 0) + mstore(add(buf_len, 0x20), 0) + mstore(add(buf_len, 0x40), 0) + mstore(add(buf_len, 0x60), 0) + buf_len := add(buf_len, 0x80) + } + + { + // Native verifier absorbs a length scalar before instance + // values; Keccak Fq transcript input is canonical BE. + // The ABI length was already checked against this generated + // constant in VkLoading.yul. + buf_len := common_word(buf_len, 19) + + let instance_cptr := INSTANCE_CPTR + for { let instance_cptr_end := add(instance_cptr, 0x0260) } + lt(instance_cptr, instance_cptr_end) + { instance_cptr := add(instance_cptr, 0x20) } { + let inst_be := calldataload(instance_cptr) + // Public inputs are BLS12-381 scalar-field elements. They + // must be canonical before transcript absorption; accepting + // non-canonical encodings would admit transcript aliases. + success := and(success, lt(inst_be, r)) + // Instances are passed BE in calldata, matching the + // Keccak Fq transcript input. + buf_len := common_word(buf_len, inst_be) + } + } + gas_checkpoint(3) // after VK digest + committed_pi + instance absorbs + + // =============================================================== + // Per-user-phase reads + challenge squeezes. + // + // Each proof G1 is already EIP-2537 padded in calldata. The + // verifier validates and absorbs that 128-byte form, then copies + // it into the corresponding per-category MPTR. The PCS / + // quotient-fold blocks below dereference those MPTRs. + // + // All G1 reads follow the same pattern: + // - common_uncompressed_g1 canonicalizes/range-checks the two Fp + // coordinates and appends the exact 128 calldata bytes; + // - calldatacopy stores the same 4-word G1 slot in planned + // memory for later EIP-2537 precompile calls; + // - proof_cptr advances by one G1 byte length. + // =============================================================== + // proof_cptr walks the raw proof bytes inside the ABI `bytes` + // payload. Every successful read advances it exactly once, and the + // final equality check below proves the parser consumed the whole + // generated proof layout. + let proof_cptr := PROOF_CPTR + // advice_walk mirrors proof commitment order into the contiguous + // G1 commitment memory region used by PCS and quotient folding. + let advice_walk := ADVICE_COMMS_MPTR_BASE + // ---- User phase 1 ---- + // Advice commitments for this phase are absorbed before the phase's + // challenge squeezes. The number of commitments and challenges is + // generated from the protocol plan. + for { let end := add(proof_cptr, 0x0780) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + // Store the commitment at its phase-ordered advice slot. + calldatacopy(advice_walk, proof_cptr, 0x80) + advice_walk := add(advice_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + } + gas_checkpoint(4) // after user-phase advice reads + user challenge squeezes + + // ---- theta ---- + // From this point onward the transcript alternates between + // squeezed challenges and proof commitments exactly as + // midnight-proofs does in `plonk/verifier.rs`. + // theta batches lookup input expressions. + buf_len := squeeze_to(buf_len, THETA_MPTR) + // ---- multiplicities (one G1 per lookup) ---- + // Lookup multiplicity commitments are absorbed after theta and + // copied into their own contiguous G1 region. + let lookup_m_walk := LOOKUP_M_COMMS_MPTR_BASE + for { let end := add(proof_cptr, 0x0100) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(lookup_m_walk, proof_cptr, 0x80) + lookup_m_walk := add(lookup_m_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + } + gas_checkpoint(5) // after theta squeeze + lookup multiplicities + + // ---- beta, gamma ---- + // beta and gamma are the permutation/lookup randomizers. They are + // squeezed after lookup multiplicities and before permutation + // product commitments, matching the native verifier schedule. + buf_len := squeeze_to(buf_len, BETA_MPTR) + buf_len := squeeze_to(buf_len, GAMMA_MPTR) + // ---- permutation Z products ---- + // Permutation product commitments are used by the permutation + // identities in the quotient numerator and later by PCS openings. + let perm_z_walk := PERM_Z_COMMS_MPTR_BASE + for { let end := add(proof_cptr, 0x0300) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(perm_z_walk, proof_cptr, 0x80) + perm_z_walk := add(perm_z_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + } + gas_checkpoint(6) // after beta/gamma + permutation Z products + // ---- lookup helpers + accumulators (per-lookup) ---- + // Each lookup contributes zero or more helper commitments followed + // by its lookup accumulator Z commitment. The generated layout keeps + // helper commitments and accumulator commitments in separate memory + // regions because the quotient/PCS schedules address them + // differently. + let lookup_helper_walk := LOOKUP_HELPER_COMMS_MPTR_BASE + let lookup_z_walk := LOOKUP_Z_COMMS_MPTR_BASE + // lookup 0: 1 helper(s) + 1 acc + // Helper commitments for lookup 0. + for { let end := add(proof_cptr, 0x80) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(lookup_helper_walk, proof_cptr, 0x80) + lookup_helper_walk := add(lookup_helper_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + } + // Accumulator commitment for lookup 0. This is + // always one G1 when the lookup section is present. + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(lookup_z_walk, proof_cptr, 0x80) + lookup_z_walk := add(lookup_z_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + // lookup 1: 1 helper(s) + 1 acc + // Helper commitments for lookup 1. + for { let end := add(proof_cptr, 0x80) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(lookup_helper_walk, proof_cptr, 0x80) + lookup_helper_walk := add(lookup_helper_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + } + // Accumulator commitment for lookup 1. This is + // always one G1 when the lookup section is present. + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(lookup_z_walk, proof_cptr, 0x80) + lookup_z_walk := add(lookup_z_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + gas_checkpoint(7) // after lookup helpers + Z accumulators + + // ---- trash_challenge ---- + // Midnight squeezes this challenge unconditionally, even when the + // circuit has no trash arguments. + // Keeping this squeeze unconditional preserves transcript + // compatibility across circuits with and without trash columns. + buf_len := squeeze_to(buf_len, TRASH_CHALLENGE_MPTR) + // ---- trashcans ---- + // Trashcan commitments are optional, but when present they are + // absorbed before y so the quotient batching challenge binds them. + let trashcan_walk := TRASHCAN_COMMS_MPTR_BASE + for { let end := add(proof_cptr, 0x80) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(trashcan_walk, proof_cptr, 0x80) + trashcan_walk := add(trashcan_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + } + gas_checkpoint(8) // after trash_challenge + trashcans + + // ---- y ---- + // y batches all quotient identities. Quotient commitments are read + // only after y is sampled, matching the Rust verifier flow. + buf_len := squeeze_to(buf_len, Y_MPTR) + + // ---- quotient commitment(s) ---- + // Each uncompressed quotient commitment is calldatacopied directly to + // QUOTIENT_LIMB_COMMS_MPTR_BASE; the Horner fold below reads + // them back from memory. common_uncompressed_g1 absorbs the + // 128-byte calldata form into the transcript verbatim. + // + // Multi-limb quotient mode reads several Q_i commitments; single-H + // mode renders this loop with one limb. + let quotient_walk := QUOTIENT_LIMB_COMMS_MPTR_BASE + for { let end := add(proof_cptr, 0x0200) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(quotient_walk, proof_cptr, 0x80) + quotient_walk := add(quotient_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + } + gas_checkpoint(9) // after y squeeze + quotient-limb reads + + // ---- x ---- + // x is the main evaluation point. Values read after this point are + // alleged polynomial evaluations at x or derived PCS openings. + buf_len := squeeze_to(buf_len, X_MPTR) + + // ---- evaluations ---- + // Optimisation H3: the off-chain Solidity proof shim rewrites + // proof scalars into BE calldata words. Spill each decoded eval + // into REVERSED_EVALS_MPTR in the same iteration we range-check + // it, so downstream references can use cheap mload. + // + // The Rust verifier conceptually reads evaluations in query order. + // The lowering plan arranges REVERSED_EVALS_MPTR in the order used + // by the quotient VM/direct evaluator, hence the generated name. + { + let eval_buf := REVERSED_EVALS_MPTR + for { let end := add(proof_cptr, 0x0cc0) } + lt(proof_cptr, end) + {} { + let eval := calldataload(proof_cptr) + // Proof evaluation scalars must be canonical Fr elements + // before they are absorbed or made available to quotient + // reconstruction. + if iszero(lt(eval, r)) { revert(0, 0) } + // Spill for quotient numerator and PCS codegen. + mstore(eval_buf, eval) + eval_buf := add(eval_buf, 0x20) + // Absorb the exact BE field word used by the native + // Keccak transcript. + buf_len := common_word(buf_len, eval) + proof_cptr := add(proof_cptr, 0x20) + } + } + + // ---- x1, x2 ---- + // x1 and x2 batch the KZG multi-opening reduction. They are + // squeezed after all polynomial evaluations are absorbed. + buf_len := squeeze_to(buf_len, X1_MPTR) + buf_len := squeeze_to(buf_len, X2_MPTR) + + // ---- f_com (1 uncompressed G1) ---- + // f_com is the commitment to the batched polynomial used by the PCS + // multi-open protocol. It is both transcript material and later + // pairing/MSM input. + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(F_COM_MPTR, proof_cptr, 0x80) + proof_cptr := add(proof_cptr, 0x80) + + // ---- x3 ---- + // x3 is the PCS evaluation point for f_com. + buf_len := squeeze_to(buf_len, X3_MPTR) + // truncated-challenges mirrors midnight-proofs + // proofs/src/poly/kzg/mod.rs: + // - x3 is the f_com evaluation point and is truncated + // immediately after squeeze. + // - x1 and x4 remain full squeezed Fr words, but later PCS + // batching stores truncate(x1^i) and truncate(x4^i) while + // keeping the internal power accumulators full precision. + // This direct x3 mask is therefore one part of the PCS truncation + // rule, not the only truncated value used by the verifier. + mstore(X3_MPTR, and(mload(X3_MPTR), 0xffffffffffffffffffffffffffffffff)) + + // ---- q_evals (one Fq per point set) ---- + // q_evals are not spilled into REVERSED_EVALS_MPTR because the PCS + // emitter reads them as a contiguous calldata range from the saved + // Q_EVAL_CPTR_MPTR cursor. + // + // Each q_eval is the claimed evaluation for one prepared point set + // in the KZG multi-open reduction. They are still transcript + // material and must be range-checked as Fr scalars. + mstore(Q_EVAL_CPTR_MPTR, proof_cptr) + for { let end := add(proof_cptr, 0xa0) } + lt(proof_cptr, end) + {} { + let eval := calldataload(proof_cptr) + // Canonical Fr check before transcript absorption. + if iszero(lt(eval, r)) { revert(0, 0) } + buf_len := common_word(buf_len, eval) + proof_cptr := add(proof_cptr, 0x20) + } + + // ---- x4 ---- + // x4 is the final PCS batching challenge, sampled after q_evals + // and before the opening proof point pi. + buf_len := squeeze_to(buf_len, X4_MPTR) + + // ---- pi (1 uncompressed G1) ---- + // pi is the KZG opening proof commitment. It is the last proof + // object absorbed into the transcript and later becomes one side of + // the final pairing check. + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(PI_MPTR, proof_cptr, 0x80) + proof_cptr := add(proof_cptr, 0x80) + + // The hand-rolled proof parser must consume exactly the ABI + // `proof` bytes before the `instances` length word. This is + // redundant with the generated proof length today, but makes + // future proof-layout drift fail closed. + // + // NUM_INSTANCE_CPTR is the calldata word immediately after the + // dynamic proof bytes payload. If proof_cptr lands anywhere else, + // some section was under-read or over-read. + if iszero(eq(proof_cptr, NUM_INSTANCE_CPTR)) { revert(0, 0) } + + // `success` carries deferred canonicality failures from public + // instance reads. G1/proof scalar helpers revert immediately. + if iszero(success) { revert(0, 0) } + gas_checkpoint(10) // after evaluations + x1/x2 + f_com + x3 + q_evals + x4 + pi (transcript done) + + // =============================================================== + // Lagrange & instance-evaluation block (pure Fr arithmetic). + // =============================================================== + { + let k := 20 + let x := mload(X_MPTR) + // Compute x^n by repeated squaring, with n = 2^k. + let x_n := x + for { let idx := 0 } lt(idx, k) { idx := add(idx, 1) } { + x_n := mulmod(x_n, x_n, r) + } + + let omega := mload(OMEGA_MPTR) + + // First pass writes denominators (x - omega_i) for every + // Lagrange value needed below, then appends x^n - 1. The + // batch inversion pass turns all of them into inverses in one + // modexp call. + let mptr := X_N_MPTR + let mptr_end := add(mptr, 0x03a0) + for { let pow_of_omega := mload(OMEGA_INV_TO_L_MPTR) } + lt(mptr, mptr_end) + { mptr := add(mptr, 0x20) } { + mstore(mptr, addmod(x, sub(r, pow_of_omega), r)) + pow_of_omega := mulmod(pow_of_omega, omega, r) + } + let x_n_minus_1 := addmod(x_n, sub(r, 1), r) + mstore(mptr_end, x_n_minus_1) + success := batch_invert(success, X_N_MPTR, add(mptr_end, 0x20), BATCH_INV_SCRATCH_MPTR, r) + + // Convert inverted denominators into Lagrange evaluations: + // L_i(x) = (x^n - 1) * n^-1 * omega_i / (x - omega_i). + mptr := X_N_MPTR + let l_i_common := mulmod(x_n_minus_1, mload(N_INV_MPTR), r) + for { let pow_of_omega := mload(OMEGA_INV_TO_L_MPTR) } + lt(mptr, mptr_end) + { mptr := add(mptr, 0x20) } { + mstore(mptr, mulmod(l_i_common, mulmod(mload(mptr), pow_of_omega, r), r)) + pow_of_omega := mulmod(pow_of_omega, omega, r) + } + + // l_blind is the sum of the negative-rotation Lagrange terms + // used by the midnight-proofs blinding identity. + let l_blind := mload(add(X_N_MPTR, 0x20)) + let l_i_cptr := add(X_N_MPTR, 0x40) + for { let l_i_cptr_end := add(X_N_MPTR, 0x0140) } + lt(l_i_cptr, l_i_cptr_end) + { l_i_cptr := add(l_i_cptr, 0x20) } { + l_blind := addmod(l_blind, mload(l_i_cptr), r) + } + + // Public instance polynomial evaluation at x. Instance words + // have already been range-checked and absorbed in transcript + // order; this loop only forms the linear combination. + let instance_eval := 0 + for { + let instance_cptr := INSTANCE_CPTR + let instance_cptr_end := add(instance_cptr, 0x0260) + } + lt(instance_cptr, instance_cptr_end) + { instance_cptr := add(instance_cptr, 0x20) + l_i_cptr := add(l_i_cptr, 0x20) } { + instance_eval := addmod(instance_eval, mulmod(mload(l_i_cptr), calldataload(instance_cptr), r), r) + } + + // Persist the derived values into named memory slots consumed + // by quotient reconstruction and PCS preparation. + let x_n_minus_1_inv := mload(mptr_end) + let l_last := mload(X_N_MPTR) + let l_0 := mload(add(X_N_MPTR, 0x0140)) + + mstore(X_N_MPTR, x_n) + mstore(X_N_MINUS_1_INV_MPTR, x_n_minus_1_inv) + mstore(L_LAST_MPTR, l_last) + mstore(L_BLIND_MPTR, l_blind) + mstore(L_0_MPTR, l_0) + mstore(INSTANCE_EVAL_MPTR, instance_eval) + } + gas_checkpoint(11) // after Lagrange + instance evaluation block + + if iszero(success) { revert(0, 0) } + + // Optional quotient helper functions. Each one is rendered only + // when the Rust lowering pass recognized the corresponding + // expression shape in this generated verifier. They are pure Fr + // helpers and share the same FR_MODULUS as the surrounding + // numerator block. + // VK-specialized identity helper for Poseidon S-box terms. + // + // Rust source shape: + // circuits/src/hash/poseidon/poseidon_chip.rs::sbox + // full_round_gate / partial_round_gate + // circuits/src/hash/poseidon/round_skips.rs::RoundId + // + // The Rust verifier only sees this as an Expression tree from + // `vk.cs.gates`; the generator emits q_pow5 after recognizing five + // equal multiplicative factors. It is a codegen shortcut for x^5, + // not a separate verifier rule. + function q_pow5(x) -> z { + let q_r := FR_MODULUS + let x2 := mulmod(x, x, q_r) + z := mulmod(x, mulmod(x2, x2, q_r), q_r) + } // =============================================================== + // Batched identity numerator / linearization target. + // + // This block does not evaluate the quotient polynomial h(x), and + // the proof does not provide an h(x) scalar to trust. Instead it: + // + // 1. Reconstructs the y-batched constraint numerator nu_y(x) + // from the alleged polynomial evaluations read after the + // transcript sampled x. + // 2. Stores -nu_y(x) as the expected opening scalar for the + // linearized commitment. + // + // The commitment side is built in the next block from the quotient + // limb commitments as (1 - x^n) * Σ_i x_split^i * Q_i, plus any + // simple-selector commitments. The PCS check later binds that + // linearized commitment to this expected scalar at x. + // + // Rust source-of-truth: + // - verifier.rs reads quotient commitments, samples x, then + // reads/computes all evaluations used below. + // - mod.rs::partially_evaluate_identities returns identities in + // gate, permutation, lookup, trash order. + // - linearization/verifier.rs::compute_linearization_commitment + // reverse-folds those identities by powers of y, sends + // simple-selector identities to selector commitment scalars, + // and subtracts fully-evaluated identities into expected_eval. + // + // This template is shared by the monolithic and external quotient + // paths. In the external path, Halo2QuotientEvaluator first copies + // the verifier memory frame into the same generated addresses. + // + // Runtime inputs expected to exist before this block starts: + // - `r` is the BLS12-381 scalar-field modulus. + // - Y_MPTR holds the quotient batching challenge y. + // - X_MPTR, L_*_MPTR, INSTANCE_EVAL_MPTR, and + // REVERSED_EVALS_MPTR hold values parsed or derived by the + // main verifier after the transcript sampled x. + // - VK_MPTR holds the pinned VK payload; in compact mode that + // payload includes the quotient constant table and bytecode. + // + // Runtime outputs written by this block: + // - QUOTIENT_EVAL_MPTR receives the scalar expected opening for + // the linearized commitment, namely -nu_y(x). + // - SELECTOR_ACC_MPTR[0..num_simple_selectors) receives one + // linearization scalar per generated simple selector. + // + // Line-by-line reading conventions used below: + // + // * Every runtime value is one canonical Fr element stored in a + // 256-bit EVM memory word. The small integer operands decoded + // from q_program are never field values; they are pointers, + // constant-table slots, selector indexes, offsets, or counts. + // + // * `mload(ptr)` is the only way the VM turns a small pointer + // operand into a real 255-bit field element. The value loaded + // from memory is then combined with `addmod(..., r)` or + // `mulmod(..., r)`, so every arithmetic line is reduced modulo + // the BLS12-381 scalar-field order. + // + // * `q_top` is the cached top of the VM operand stack. When an + // opcode needs to push while `q_top` is already live, the old + // value is written to `q_sp` and `q_sp` is advanced by one + // word. Binary `ADD`/`MUL` move `q_sp` back by one word and + // combine that spilled value with `q_top`. + // + // * Identity boundaries are explicit. Expression opcodes leave + // one value in `q_top`; `FOLD_MAIN` or `FOLD_SELECTOR` consumes + // it and advances the global y-batch position. Native callback + // opcodes are only emitted at empty-stack boundaries and run + // generated Yul that performs the same fold side effects. + // + // * The generated Solidity source intentionally emits comments + // before opcode cases. Those comments are documentation only: + // they do not affect bytecode, but they make rendered verifier + // assembly readable without jumping back to Rust codegen. + // =============================================================== + { + // Compact quotient-program mode. + // + // The largest identity expressions are not all emitted as + // unrolled Yul. Instead, most arithmetic is encoded as a small + // q_program bytecode stored in the VK payload. This block + // interprets that program, while selected heavy identities may + // still be emitted as native callbacks for gas. + // + // Compact mode is a code-size trade: short bytecode operands + // name already-planned memory slots, and the interpreter turns + // those names into Fr arithmetic. The opcode stream is fully + // generated and pinned by the VK/runtime codehash; no proof + // calldata can alter control flow. + // Load the quotient batching challenge used by every fold. + let y := mload(Y_MPTR) + + // q_const_mptr points to Fr constants used by the VM. + // q_program_mptr points to the bytecode stream. + // Constants are stored as consecutive 32-byte Fr words. + let q_const_mptr := 0x2ae0 + // Program bytes are also stored in the VK payload, packed into + // 32-byte words by PackedProgramCodec. + let q_program_mptr := 0x4120 + // Running Horner accumulator for fully evaluated identities. + // After all identities, this is nu_y(x) for the `None` + // identity group. + // Initialize A = 0 before scanning the identity stream. + mstore(0xa300, 0) + // Simple selectors are grouped into separate linearization + // buckets. They start at zero for every proof. + // q_sel_zero_off walks selector bucket byte offsets. + for { let q_sel_zero_off := 0 } lt(q_sel_zero_off, 0x0140) { q_sel_zero_off := add(q_sel_zero_off, 0x20) } { + // B_s = 0 for each simple selector bucket. + mstore(add(SELECTOR_ACC_MPTR, q_sel_zero_off), 0) + } + // Codegen knows the selector identity positions. Precompute + // the y^k powers needed for selector gap and tail updates, + // avoiding a runtime y^-1 modexp and per-identity selector + // scale maintenance. + { + // q_y_power holds y^i at the current loop index. + let q_y_power := 1 + // Start at i=1 because y^0 = 1 is implicit and never read. + for { let q_y_power_i := 1 } lt(q_y_power_i, 49) { q_y_power_i := add(q_y_power_i, 1) } { + // Advance from y^(i-1) to y^i modulo Fr. + q_y_power := mulmod(q_y_power, y, r) + // Store y^i at selector_power_mptr + 32*i. + mstore(add(0xa340, shl(5, q_y_power_i)), q_y_power) + } + } + + // Direct inline prefix. These identities are generated as Yul + // before entering the VM. They use the same fold snippets as + // VM/native identities, so they occupy the same y-batch order. + { + let var0 := 0x1 + let f_3 := mload(0x8b40) + let f_4 := mload(0x8a40) + let a_0 := mload(0x8520) + let var1 := mulmod(f_4, a_0, r) + let var2 := addmod(f_3, var1, r) + let f_5 := mload(0x8a60) + let a_1 := mload(0x8540) + let var3 := mulmod(f_5, a_1, r) + let var4 := addmod(var2, var3, r) + let f_6 := mload(0x8a80) + let a_2 := mload(0x8560) + let var5 := mulmod(f_6, a_2, r) + let var6 := addmod(var4, var5, r) + let f_7 := mload(0x8aa0) + let a_3 := mload(0x8580) + let var7 := mulmod(f_7, a_3, r) + let var8 := addmod(var6, var7, r) + let f_8 := mload(0x8ac0) + let a_4 := mload(0x85a0) + let var9 := mulmod(f_8, a_4, r) + let var10 := addmod(var8, var9, r) + let f_0 := mload(0x8ae0) + let a_0_next_1 := mload(0x85c0) + let var11 := mulmod(f_0, a_0_next_1, r) + let var12 := addmod(var10, var11, r) + let f_1 := mload(0x8b00) + let var13 := mulmod(f_1, a_0, r) + let var14 := mulmod(var13, a_1, r) + let var15 := addmod(var12, var14, r) + let f_2 := mload(0x8b20) + let var16 := mulmod(f_2, a_0, r) + let var17 := mulmod(var16, a_2, r) + let var18 := addmod(var15, var17, r) + let var19 := mulmod(var0, var18, r) + mstore(0xa960, var19) + } + mstore(0xa300, mulmod(mload(0xa300), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x0) + let q_selector_acc := mload(q_selector_ptr) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xa960), r)) + } + { + let var0 := 0x1 + let a_1 := mload(0x8540) + let a_2 := mload(0x8560) + let var1 := addmod(a_1, a_2, r) + let a_3 := mload(0x8580) + let var2 := addmod(0, sub(r, a_3), r) + let var3 := addmod(var1, var2, r) + let a_4 := mload(0x85a0) + let var4 := addmod(0, sub(r, a_4), r) + let var5 := addmod(var3, var4, r) + let var6 := mulmod(var0, var5, r) + mstore(0xa960, var6) + } + mstore(0xa300, mulmod(mload(0xa300), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x20) + let q_selector_acc := mload(q_selector_ptr) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xa960), r)) + } + { + let var0 := 0x1 + let a_0 := mload(0x8520) + let f_4 := mload(0x8a40) + let var1 := addmod(a_0, f_4, r) + let a_0_next_1 := mload(0x85c0) + let var2 := addmod(0, sub(r, a_0_next_1), r) + let var3 := addmod(var1, var2, r) + let var4 := mulmod(var0, var3, r) + mstore(0xa960, var4) + } + mstore(0xa300, mulmod(mload(0xa300), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x40) + let q_selector_acc := mload(q_selector_ptr) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xa960), r)) + } + { + let var0 := 0x1 + let a_1 := mload(0x8540) + let f_5 := mload(0x8a60) + let var1 := addmod(a_1, f_5, r) + let a_1_next_1 := mload(0x85e0) + let var2 := addmod(0, sub(r, a_1_next_1), r) + let var3 := addmod(var1, var2, r) + let var4 := mulmod(var0, var3, r) + mstore(0xa960, var4) + } + mstore(0xa300, mulmod(mload(0xa300), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x40) + let q_selector_acc := mload(q_selector_ptr) + q_selector_acc := mulmod(q_selector_acc, mload(add(0xa340, 0x20)), r) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xa960), r)) + } + + // VM registers: + // q_pc current bytecode pointer + // q_end end of bytecode stream + // q_sp memory stack pointer for non-top stack values + // q_top cached top-of-stack value + // q_has_top whether q_top currently holds a stack value + // + // The cached top reduces memory traffic in the interpreter. + // q_sp's registered range must cover the interpreted operand + // stack plus any native callback scratch that reuses this base + // pointer. In particular, the native permutation callback + // writes a structured scratch table at program.stack_mptr. + // q_pc starts at the first encoded instruction. + let q_pc := q_program_mptr + // q_end is an exclusive byte pointer for the VM loop. + let q_end := add(q_program_mptr, 0x11cf) + // q_sp starts at the first free stack word. + let q_sp := 0xa960 + // q_top is meaningless until q_has_top is set. + let q_top := 0 + // q_has_top = 0 means the VM stack is empty. + let q_has_top := 0 + + // q_program opcode summary: + // 0x01/0x09 push const 0x02/0x05 push memory + // 0x03/0x04 push token ptr 0x06 add, 0x07 mul, 0x08 neg + // 0x0a fold main identity 0x0b fold selector identity + // 0x0c..0x11 add/mul const or memory into top + // 0x12..0x16 fused add-mul runs + // 0x17/0x18 reserved + // 0x19 native permutation 0x1b native heavy identity + // 0x1c LIN7 0x1d BILIN7_ROW + // 0x1e BILIN7_PAIRWISE 0x1f native lookup + // 0x20 POW5 0x21 MODARITH7 + // 0x22 AFFINE_SUM + // + // The default IVC verifier uses one physical encoding for the + // logical VM: compact byte-oriented opcodes with variable-width + // operands, dynamic runs, and limb-aware cases. + + // Byte-oriented encoding: opcodes are one byte followed by + // variable-width operand bytes. + for { } lt(q_pc, q_end) { } { + // The bytecode table is byte-addressed, but EVM memory + // loads whole words. `byte(0, mload(q_pc))` extracts the + // opcode at the current byte cursor; each case advances + // q_pc by exactly its operand width. + let q_op := byte(0, mload(q_pc)) + q_pc := add(q_pc, 1) + + switch q_op + // VM 0x05 PUSH_MEM_U16 (bytes): next two bytes are a short memory pointer. + case 0x05 { + // Operand layout: u16 absolute memory pointer. The + // memory planner keeps the hot quotient frame below + // 64 KiB when this compact form is emitted. + let q_ptr := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + if q_has_top { + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } + q_top := mload(q_ptr) + q_has_top := 1 + } + // VM 0x06 ADD: pop one spilled stack word and add it to q_top. + case 0x06 { + // The safety validator guarantees a spilled operand + // exists before ADD. q_top is the right operand. + q_sp := sub(q_sp, 0x20) + q_top := addmod(mload(q_sp), q_top, r) + } + // VM 0x08 NEG: replace q_top with its Fr negation. + case 0x08 { + // addmod(0, r - x, r) maps zero back to zero and every + // nonzero scalar to its canonical additive inverse. + q_top := addmod(0, sub(r, q_top), r) + } + // VM 0x0d MUL_CONST_U8: multiply q_top by a small constant-table slot. + case 0x0d { + // One-byte constant-index multiply, used by short + // affine chains after an initial PUSH. + let qconst := byte(0, mload(q_pc)) + q_pc := add(q_pc, 1) + q_top := mulmod(q_top, mload(add(q_const_mptr, shl(5, qconst))), r) + } + // VM 0x10 ADD_MEM_U16: add a short memory load into q_top. + case 0x10 { + // Operand layout: u16 pointer. The pointed word is an + // already range-checked Fr scalar in verifier memory. + let q_ptr := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + q_top := addmod(q_top, mload(q_ptr), r) + } + // VM 0x11 MUL_MEM_U16: multiply q_top by a short memory load. + case 0x11 { + // In-place multiply by a planned memory word. + let q_ptr := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + q_top := mulmod(q_top, mload(q_ptr), r) + } + // Limb-aware opcodes are opt-in compact forms for + // structurally recognized non-SHA foreign-field shapes. + // Coefficients are indexes into q_const_mptr, which is + // generated from VK/program data, never from proof + // calldata. + // + // Rust source shape: + // proofs/src/plonk/mod.rs::partially_evaluate_identities + // circuits/src/field/foreign/util.rs::{sum_exprs,pair_wise_prod} + // circuits/src/field/foreign/params.rs::{base_powers,double_base_powers} + // + // "Foreign field" means the circuit represents elements + // modulo another modulus m as 7 limbs in base + // 2^LOG2_BASE. The verifier does not switch fields; it + // evaluates the lowered identity over BLS12-381 Fr, using + // Fr coefficients equal to base^i mod m or base^(i+j) mod m. + // VM 0x21 MODARITH7: byte-only fused affine 7-limb foreign-field/ECC identity. + case 0x21 { + // MODARITH7: + // maybe_cond * ( + // c + // + sum LIN7 blocks + // + sum BILIN7_ROW blocks + // + sum BILIN7_PAIRWISE blocks + // + sum coeff[k] * mload(ptr[k]) + // + sum coeff[k] * mload(lhs[k]) * mload(rhs[k]) + // ) + // It is a dispatch/operand-load optimization only; + // all coefficients still come from the generated + // quotient constant table. + // + // Flags: + // bit 0: multiply the final affine sum by a memory + // condition word. + // bit 1: seed q_acc from a constant-table word + // before reading the counted term blocks. + let q_flags := byte(0, mload(q_pc)) + q_pc := add(q_pc, 1) + let q_cond_ptr := 0 + if and(q_flags, 0x01) { + // Optional condition pointer. When present, the + // whole identity is gated by mload(q_cond_ptr). + q_cond_ptr := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + } + + let q_acc := 0 + if and(q_flags, 0x02) { + // Optional constant seed for affine identities + // with a standalone constant term. + let qconst := byte(0, mload(q_pc)) + q_pc := add(q_pc, 1) + q_acc := mload(add(q_const_mptr, shl(5, qconst))) + } + + // Five one-byte counters describe the blocks that + // follow. Each block has a fixed-width internal layout, + // so q_pc can advance without per-term tags. + let q_counts_word := mload(q_pc) + let q_lin_count := byte(0, q_counts_word) + let q_row_count := byte(1, q_counts_word) + let q_pairwise_count := byte(2, q_counts_word) + let q_mem_count := byte(3, q_counts_word) + let q_product_count := byte(4, q_counts_word) + q_pc := add(q_pc, 5) + + if q_has_top { + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } + + // LIN7 blocks: q_acc += sum_i c_i * limb_i. + for { let q_lin_block := 0 } lt(q_lin_block, q_lin_count) { q_lin_block := add(q_lin_block, 1) } { + for { let q_i := 0 } lt(q_i, 7) { q_i := add(q_i, 1) } { + let q_word := mload(q_pc) + let qconst := byte(0, q_word) + let q_ptr := and(shr(232, q_word), 0xffff) + q_pc := add(q_pc, 3) + q_acc := addmod( + q_acc, + mulmod(mload(add(q_const_mptr, shl(5, qconst))), mload(q_ptr), r), + r + ) + } + } + + // BILIN7_ROW blocks: q_acc += lhs * sum_i c_i * rhs_i. + for { let q_row_block := 0 } lt(q_row_block, q_row_count) { q_row_block := add(q_row_block, 1) } { + let q_lhs := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + let q_lhs_value := mload(q_lhs) + for { let q_i := 0 } lt(q_i, 7) { q_i := add(q_i, 1) } { + let q_word := mload(q_pc) + let qconst := byte(0, q_word) + let q_rhs := and(shr(232, q_word), 0xffff) + q_pc := add(q_pc, 3) + q_acc := addmod( + q_acc, + mulmod( + mulmod(q_lhs_value, mload(q_rhs), r), + mload(add(q_const_mptr, shl(5, qconst))), + r + ), + r + ) + } + } + + // BILIN7_PAIRWISE blocks: q_acc += weighted 7-by-7 + // product convolution. + for { let q_pair_block := 0 } lt(q_pair_block, q_pairwise_count) { q_pair_block := add(q_pair_block, 1) } { + let q_pair_word := mload(q_pc) + let q_lhs_base := shr(240, q_pair_word) + let q_rhs_base := and(shr(224, q_pair_word), 0xffff) + q_pc := add(q_pc, 0x04) + let q_coeff_pc := q_pc + q_pc := add(q_pc, 13) + for { let q_i := 0 } lt(q_i, 7) { q_i := add(q_i, 1) } { + let q_lhs_value := mload(add(q_lhs_base, shl(5, q_i))) + for { let q_j := 0 } lt(q_j, 7) { q_j := add(q_j, 1) } { + let qconst := byte(0, mload(add(q_coeff_pc, add(q_i, q_j)))) + q_acc := addmod( + q_acc, + mulmod( + mulmod(q_lhs_value, mload(add(q_rhs_base, shl(5, q_j))), r), + mload(add(q_const_mptr, shl(5, qconst))), + r + ), + r + ) + } + } + } + + // Extra linear memory terms outside the 7-limb shapes. + for { let q_mem_block := 0 } lt(q_mem_block, q_mem_count) { q_mem_block := add(q_mem_block, 1) } { + let q_word := mload(q_pc) + let qconst := byte(0, q_word) + let q_ptr := and(shr(232, q_word), 0xffff) + q_pc := add(q_pc, 3) + q_acc := addmod( + q_acc, + mulmod(mload(add(q_const_mptr, shl(5, qconst))), mload(q_ptr), r), + r + ) + } + + // Extra binary product terms outside the 7-limb shapes. + for { let q_product_block := 0 } lt(q_product_block, q_product_count) { q_product_block := add(q_product_block, 1) } { + let q_word := mload(q_pc) + let qconst := byte(0, q_word) + let q_lhs := and(shr(232, q_word), 0xffff) + let q_rhs := and(shr(216, q_word), 0xffff) + q_pc := add(q_pc, 5) + q_acc := addmod( + q_acc, + mulmod( + mulmod(mload(q_lhs), mload(q_rhs), r), + mload(add(q_const_mptr, shl(5, qconst))), + r + ), + r + ) + } + + if and(q_flags, 0x01) { + // Apply the optional gate condition last so every + // subterm shares the same selector/condition. + q_acc := mulmod(mload(q_cond_ptr), q_acc, r) + } + // MODARITH7 pushes its fused identity value. + q_top := q_acc + q_has_top := 1 + } + // Native permutation callback. It evaluates the + // permutation identities from permutation.rs at this exact + // VM position, preserving the Rust identity order while + // avoiding a large interpreted product loop. + // VM 0x19 NATIVE_PERMUTATION: marker for the generated permutation callback. + case 0x19 { + // Native callbacks are identity-boundary opcodes. They + // must not inherit any partially evaluated VM stack + // state from the previous expression. + q_top := 0 + q_has_top := 0 + // The generated loop below uses program.stack_mptr as + // its scratch-table base, not as a conventional VM + // stack. The Rust memory planner must reserve enough + // words for structured_permutation_scratch_words(meta) + // whenever this opcode can appear. + q_sp := 0xa960 + // The generated lines below call the same fold snippets + // used by interpreted expressions, so trace IDs and + // y-batch positions remain contiguous. + { + let delta := 0x8634d0aa021aaf843cab354fabb0062f6502437c6a09c006c083479590189d7 + let q_perm_vals := 0xa960 + let q_perm_sigmas := 0xaba0 + let q_perm_z_cur := 0xade0 + let q_perm_z_next := 0xaea0 + let q_perm_z_last := 0xaf60 + let q_perm_delta_base_ptr := 0xb000 + let q_perm_num_cols := 18 + let q_perm_num_sets := 6 + let q_perm_chunk_len := 3 + let q_perm_delta_chunk := 0x4285088329c399ea457a8ca1d30f8957e74c7f529842a1579b4fee55b3982923 + mstore(add(q_perm_vals, 0x0), mload(0x8a20)) + { + for { let q_perm_val_load_i := 0 } lt(q_perm_val_load_i, 5) { q_perm_val_load_i := add(q_perm_val_load_i, 1) } { + let q_perm_val_load_dst_off := shl(5, q_perm_val_load_i) + let q_perm_val_load_src_off := q_perm_val_load_dst_off + mstore(add(add(q_perm_vals, 0x20), q_perm_val_load_dst_off), mload(add(0x8520, q_perm_val_load_src_off))) + } + } + mstore(add(q_perm_vals, 0xc0), mload(0x8500)) + mstore(add(q_perm_vals, 0xe0), mload(INSTANCE_EVAL_MPTR)) + { + for { let q_perm_val_load_i := 0 } lt(q_perm_val_load_i, 9) { q_perm_val_load_i := add(q_perm_val_load_i, 1) } { + let q_perm_val_load_dst_off := shl(5, q_perm_val_load_i) + let q_perm_val_load_src_off := q_perm_val_load_dst_off + mstore(add(add(q_perm_vals, 0x100), q_perm_val_load_dst_off), mload(add(0x8620, q_perm_val_load_src_off))) + } + } + mstore(add(q_perm_vals, 0x220), mload(0x8a00)) + { + for { let q_perm_sigma_load_i := 0 } lt(q_perm_sigma_load_i, 18) { q_perm_sigma_load_i := add(q_perm_sigma_load_i, 1) } { + let q_perm_sigma_load_dst_off := shl(5, q_perm_sigma_load_i) + let q_perm_sigma_load_src_off := q_perm_sigma_load_dst_off + mstore(add(add(q_perm_sigmas, 0x0), q_perm_sigma_load_dst_off), mload(add(0x8c40, q_perm_sigma_load_src_off))) + } + } + { + for { let q_perm_z_cur_load_i := 0 } lt(q_perm_z_cur_load_i, 6) { q_perm_z_cur_load_i := add(q_perm_z_cur_load_i, 1) } { + let q_perm_z_cur_load_dst_off := shl(5, q_perm_z_cur_load_i) + let q_perm_z_cur_load_src_off := mul(q_perm_z_cur_load_i, 0x60) + mstore(add(add(q_perm_z_cur, 0x0), q_perm_z_cur_load_dst_off), mload(add(0x8e80, q_perm_z_cur_load_src_off))) + } + } + { + for { let q_perm_z_next_load_i := 0 } lt(q_perm_z_next_load_i, 6) { q_perm_z_next_load_i := add(q_perm_z_next_load_i, 1) } { + let q_perm_z_next_load_dst_off := shl(5, q_perm_z_next_load_i) + let q_perm_z_next_load_src_off := mul(q_perm_z_next_load_i, 0x60) + mstore(add(add(q_perm_z_next, 0x0), q_perm_z_next_load_dst_off), mload(add(0x8ea0, q_perm_z_next_load_src_off))) + } + } + { + for { let q_perm_z_last_load_i := 0 } lt(q_perm_z_last_load_i, 5) { q_perm_z_last_load_i := add(q_perm_z_last_load_i, 1) } { + let q_perm_z_last_load_dst_off := shl(5, q_perm_z_last_load_i) + let q_perm_z_last_load_src_off := mul(q_perm_z_last_load_i, 0x60) + mstore(add(add(q_perm_z_last, 0x0), q_perm_z_last_load_dst_off), mload(add(0x8ec0, q_perm_z_last_load_src_off))) + } + } + let q_perm_eval := 0 + q_perm_eval := mulmod(mload(L_0_MPTR), addmod(1, sub(r, mload(q_perm_z_cur)), r), r) + mstore(0xa300, mulmod(mload(0xa300), y, r)) + mstore(0xa300, addmod(mload(0xa300), q_perm_eval, r)) + let q_perm_zn := mload(add(q_perm_z_cur, 0xa0)) + q_perm_eval := mulmod(mload(L_LAST_MPTR), addmod(mulmod(q_perm_zn, q_perm_zn, r), sub(r, q_perm_zn), r), r) + mstore(0xa300, mulmod(mload(0xa300), y, r)) + mstore(0xa300, addmod(mload(0xa300), q_perm_eval, r)) + for { let q_perm_i := 1 } lt(q_perm_i, 6) { q_perm_i := add(q_perm_i, 1) } { + let q_perm_cur := mload(add(q_perm_z_cur, shl(5, q_perm_i))) + let q_perm_prev := mload(add(q_perm_z_last, shl(5, sub(q_perm_i, 1)))) + q_perm_eval := mulmod(mload(L_0_MPTR), addmod(q_perm_cur, sub(r, q_perm_prev), r), r) + mstore(0xa300, mulmod(mload(0xa300), y, r)) + mstore(0xa300, addmod(mload(0xa300), q_perm_eval, r)) + } + mstore(q_perm_delta_base_ptr, mulmod(mload(BETA_MPTR), mload(X_MPTR), r)) + for { let q_perm_set := 0 } lt(q_perm_set, 6) { q_perm_set := add(q_perm_set, 1) } { + let q_perm_start := mul(q_perm_set, q_perm_chunk_len) + let q_perm_end := add(q_perm_start, q_perm_chunk_len) + if gt(q_perm_end, q_perm_num_cols) { q_perm_end := q_perm_num_cols } + let q_perm_left := mload(add(q_perm_z_next, shl(5, q_perm_set))) + let q_perm_right := mload(add(q_perm_z_cur, shl(5, q_perm_set))) + let q_perm_delta_pow := mload(q_perm_delta_base_ptr) + for { let q_perm_j := q_perm_start } lt(q_perm_j, q_perm_end) { q_perm_j := add(q_perm_j, 1) } { + let q_perm_off := shl(5, q_perm_j) + let q_perm_v := mload(add(q_perm_vals, q_perm_off)) + let q_perm_s := mload(add(q_perm_sigmas, q_perm_off)) + q_perm_left := mulmod(q_perm_left, addmod(addmod(q_perm_v, mulmod(mload(BETA_MPTR), q_perm_s, r), r), mload(GAMMA_MPTR), r), r) + q_perm_right := mulmod(q_perm_right, addmod(addmod(q_perm_v, q_perm_delta_pow, r), mload(GAMMA_MPTR), r), r) + q_perm_delta_pow := mulmod(q_perm_delta_pow, delta, r) + } + q_perm_eval := mulmod(addmod(1, sub(r, addmod(mload(L_LAST_MPTR), mload(L_BLIND_MPTR), r)), r), addmod(q_perm_left, sub(r, q_perm_right), r), r) + mstore(0xa300, mulmod(mload(0xa300), y, r)) + mstore(0xa300, addmod(mload(0xa300), q_perm_eval, r)) + mstore(q_perm_delta_base_ptr, mulmod(mload(q_perm_delta_base_ptr), q_perm_delta_chunk, r)) + } + } + } + // Native lookup callback. This whole-family opcode + // evaluates the LogUp boundary, helper-chunk, and + // accumulator identities at this VM position, preserving + // the Rust y-batch order while avoiding many interpreted + // product-loop opcodes. + // VM 0x1f NATIVE_LOOKUP: marker for the generated LogUp lookup callback. + case 0x1f { + // Reset VM stack state before entering structured + // lookup Yul. Lookup callbacks own their scratch + // layout and perform all needed folds internally. + q_top := 0 + q_has_top := 0 + // The generated loop below uses program.stack_mptr as + // f+beta/prefix/suffix scratch rather than as a + // conventional VM stack. The Rust memory planner must + // reserve structured_lookup_scratch_words(meta). + q_sp := 0xa960 + // Generated LogUp code follows the same y-batch order + // as the Rust identity stream. + { + let q_lookup_f := 0xa960 + let q_lookup_prefix := 0xa9e0 + let q_lookup_suffix := 0xaa60 + let q_lookup_l0 := mload(L_0_MPTR) + let q_lookup_llast := mload(L_LAST_MPTR) + let q_lookup_lblind := mload(L_BLIND_MPTR) + let q_lookup_lsum := addmod(q_lookup_l0, q_lookup_llast, r) + let q_lookup_active := addmod(1, sub(r, addmod(q_lookup_llast, q_lookup_lblind, r)), r) + let q_lookup_beta := mload(BETA_MPTR) + let q_lookup_theta := mload(THETA_MPTR) + { + { + let q_lookup_eval := mulmod(q_lookup_lsum, mload(0x90e0), r) + mstore(0xa300, mulmod(mload(0xa300), y, r)) + mstore(0xa300, addmod(mload(0xa300), q_lookup_eval, r)) + } + { + let f_10 := mload(0x8b60) + let var0 := addmod(mulmod(0, q_lookup_theta, r), f_10, r) + let var1 := mulmod(var0, q_lookup_theta, r) + for { let q_lookup_shared_i := 0 } lt(q_lookup_shared_i, 4) { q_lookup_shared_i := add(q_lookup_shared_i, 1) } { + let q_lookup_shared_off := shl(5, q_lookup_shared_i) + let q_lookup_shared_tail := mload(add(0x8540, q_lookup_shared_off)) + let q_lookup_shared_compressed := addmod(var1, q_lookup_shared_tail, r) + mstore(add(q_lookup_f, q_lookup_shared_off), addmod(q_lookup_shared_compressed, q_lookup_beta, r)) + } + let q_lookup_product := 1 + for { let q_lookup_prod_i := 0 } lt(q_lookup_prod_i, 4) { q_lookup_prod_i := add(q_lookup_prod_i, 1) } { + q_lookup_product := mulmod(q_lookup_product, mload(add(q_lookup_f, shl(5, q_lookup_prod_i))), r) + } + mstore(q_lookup_prefix, 1) + for { let q_lookup_pref_i := 1 } lt(q_lookup_pref_i, 4) { q_lookup_pref_i := add(q_lookup_pref_i, 1) } { + let q_lookup_pref_prev := sub(q_lookup_pref_i, 1) + mstore(add(q_lookup_prefix, shl(5, q_lookup_pref_i)), mulmod(mload(add(q_lookup_prefix, shl(5, q_lookup_pref_prev))), mload(add(q_lookup_f, shl(5, q_lookup_pref_prev))), r)) + } + mstore(add(q_lookup_suffix, 0x60), 1) + for { let q_lookup_suf_i := sub(4, 1) } gt(q_lookup_suf_i, 0) { q_lookup_suf_i := sub(q_lookup_suf_i, 1) } { + let q_lookup_suf_prev := sub(q_lookup_suf_i, 1) + mstore(add(q_lookup_suffix, shl(5, q_lookup_suf_prev)), mulmod(mload(add(q_lookup_suffix, shl(5, q_lookup_suf_i))), mload(add(q_lookup_f, shl(5, q_lookup_suf_i))), r)) + } + let q_lookup_sum := 0 + for { let q_lookup_sum_i := 0 } lt(q_lookup_sum_i, 4) { q_lookup_sum_i := add(q_lookup_sum_i, 1) } { + q_lookup_sum := addmod(q_lookup_sum, mulmod(mload(add(q_lookup_prefix, shl(5, q_lookup_sum_i))), mload(add(q_lookup_suffix, shl(5, q_lookup_sum_i))), r), r) + } + let q_lookup_eval := addmod(mulmod(mload(0x90c0), q_lookup_product, r), sub(r, q_lookup_sum), r) + mstore(0xa300, mulmod(mload(0xa300), y, r)) + mstore(0xa300, addmod(mload(0xa300), q_lookup_eval, r)) + } + { + let q_lookup_sum_h := mload(0x90c0) + let f_17 := mload(0x8be0) + let f_11 := mload(0x8b80) + let var0 := addmod(mulmod(0, q_lookup_theta, r), f_11, r) + let f_12 := mload(0x8ba0) + let var1 := addmod(mulmod(var0, q_lookup_theta, r), f_12, r) + let q_lookup_s_sum_h := mulmod(f_17, q_lookup_sum_h, r) + let q_lookup_diff := addmod(mload(0x9100), sub(r, addmod(mload(0x90e0), q_lookup_s_sum_h, r)), r) + let q_lookup_t_beta := addmod(var1, q_lookup_beta, r) + let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), mload(0x90a0), r) + let q_lookup_eval := mulmod(q_lookup_active, q_lookup_core, r) + mstore(0xa300, mulmod(mload(0xa300), y, r)) + mstore(0xa300, addmod(mload(0xa300), q_lookup_eval, r)) + } + } + { + { + let q_lookup_eval := mulmod(q_lookup_lsum, mload(0x9160), r) + mstore(0xa300, mulmod(mload(0xa300), y, r)) + mstore(0xa300, addmod(mload(0xa300), q_lookup_eval, r)) + } + { + let a_14 := mload(0x8a00) + let var0 := addmod(mulmod(0, q_lookup_theta, r), a_14, r) + let a_0 := mload(0x8520) + let var1 := addmod(mulmod(var0, q_lookup_theta, r), a_0, r) + let a_1 := mload(0x8540) + let var2 := addmod(mulmod(var1, q_lookup_theta, r), a_1, r) + let a_2 := mload(0x8560) + let var3 := addmod(mulmod(var2, q_lookup_theta, r), a_2, r) + let a_3 := mload(0x8580) + let var4 := addmod(mulmod(var3, q_lookup_theta, r), a_3, r) + let a_4 := mload(0x85a0) + let var5 := addmod(mulmod(var4, q_lookup_theta, r), a_4, r) + let a_5 := mload(0x8620) + let var6 := addmod(mulmod(var5, q_lookup_theta, r), a_5, r) + let a_6 := mload(0x8640) + let var7 := addmod(mulmod(var6, q_lookup_theta, r), a_6, r) + let a_7 := mload(0x8660) + let var8 := addmod(mulmod(var7, q_lookup_theta, r), a_7, r) + let a_8 := mload(0x8680) + let var9 := addmod(mulmod(var8, q_lookup_theta, r), a_8, r) + let a_9 := mload(0x86a0) + let var10 := addmod(mulmod(var9, q_lookup_theta, r), a_9, r) + let a_10 := mload(0x86c0) + let var11 := addmod(mulmod(var10, q_lookup_theta, r), a_10, r) + let a_11 := mload(0x86e0) + let var12 := addmod(mulmod(var11, q_lookup_theta, r), a_11, r) + let a_12 := mload(0x8700) + let var13 := addmod(mulmod(var12, q_lookup_theta, r), a_12, r) + let a_13 := mload(0x8720) + let var14 := addmod(mulmod(var13, q_lookup_theta, r), a_13, r) + let f_13 := mload(0x8bc0) + let var15 := addmod(mulmod(var14, q_lookup_theta, r), f_13, r) + let q_lookup_eval := addmod(mulmod(mload(0x9140), addmod(var15, q_lookup_beta, r), r), sub(r, 1), r) + mstore(0xa300, mulmod(mload(0xa300), y, r)) + mstore(0xa300, addmod(mload(0xa300), q_lookup_eval, r)) + } + { + let q_lookup_sum_h := mload(0x9140) + let var0 := 0x1 + let f_24 := mload(0x8c00) + let var1 := addmod(0, sub(r, f_24), r) + let var2 := addmod(var0, var1, r) + let a_14 := mload(0x8a00) + let var3 := mulmod(var2, a_14, r) + let var4 := addmod(mulmod(0, q_lookup_theta, r), var3, r) + let a_0 := mload(0x8520) + let var5 := mulmod(var2, a_0, r) + let var6 := addmod(mulmod(var4, q_lookup_theta, r), var5, r) + let a_1 := mload(0x8540) + let var7 := mulmod(var2, a_1, r) + let var8 := addmod(mulmod(var6, q_lookup_theta, r), var7, r) + let a_2 := mload(0x8560) + let var9 := mulmod(var2, a_2, r) + let var10 := addmod(mulmod(var8, q_lookup_theta, r), var9, r) + let a_3 := mload(0x8580) + let var11 := mulmod(var2, a_3, r) + let var12 := addmod(mulmod(var10, q_lookup_theta, r), var11, r) + let a_4 := mload(0x85a0) + let var13 := mulmod(var2, a_4, r) + let var14 := addmod(mulmod(var12, q_lookup_theta, r), var13, r) + let a_5 := mload(0x8620) + let var15 := mulmod(var2, a_5, r) + let var16 := addmod(mulmod(var14, q_lookup_theta, r), var15, r) + let a_6 := mload(0x8640) + let var17 := mulmod(var2, a_6, r) + let var18 := addmod(mulmod(var16, q_lookup_theta, r), var17, r) + let a_7 := mload(0x8660) + let var19 := mulmod(var2, a_7, r) + let var20 := addmod(mulmod(var18, q_lookup_theta, r), var19, r) + let a_8 := mload(0x8680) + let var21 := mulmod(var2, a_8, r) + let var22 := addmod(mulmod(var20, q_lookup_theta, r), var21, r) + let a_9 := mload(0x86a0) + let var23 := mulmod(var2, a_9, r) + let var24 := addmod(mulmod(var22, q_lookup_theta, r), var23, r) + let a_10 := mload(0x86c0) + let var25 := mulmod(var2, a_10, r) + let var26 := addmod(mulmod(var24, q_lookup_theta, r), var25, r) + let a_11 := mload(0x86e0) + let var27 := mulmod(var2, a_11, r) + let var28 := addmod(mulmod(var26, q_lookup_theta, r), var27, r) + let a_12 := mload(0x8700) + let var29 := mulmod(var2, a_12, r) + let var30 := addmod(mulmod(var28, q_lookup_theta, r), var29, r) + let a_13 := mload(0x8720) + let var31 := mulmod(var2, a_13, r) + let var32 := addmod(mulmod(var30, q_lookup_theta, r), var31, r) + let f_13 := mload(0x8bc0) + let var33 := mulmod(var2, f_13, r) + let var34 := addmod(mulmod(var32, q_lookup_theta, r), var33, r) + let q_lookup_s_sum_h := mulmod(var0, q_lookup_sum_h, r) + let q_lookup_diff := addmod(mload(0x9180), sub(r, addmod(mload(0x9160), q_lookup_s_sum_h, r)), r) + let q_lookup_t_beta := addmod(var34, q_lookup_beta, r) + let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), mload(0x9120), r) + let q_lookup_eval := mulmod(q_lookup_active, q_lookup_core, r) + mstore(0xa300, mulmod(mload(0xa300), y, r)) + mstore(0xa300, addmod(mload(0xa300), q_lookup_eval, r)) + } + } + } + } + // Native callbacks are generated only for the heaviest + // recognized Midfall gate identities. All other gate and + // non-native identity arithmetic remains in + // the compact q_program VM above, preserving the Rust + // `partially_evaluate_identities` order. + // VM 0x1b NATIVE_IDENTITY: marker for generated heavy-gate callbacks. + case 0x1b { + // Operand layout: u16 native callback index. The + // manifest validates that callback indexes appear in + // generated order and target existing switch cases. + let q_native_idx := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + // Heavy identities are whole expressions, so clear the + // interpreter stack before dispatching. + q_top := 0 + q_has_top := 0 + q_sp := 0xa960 + // Native identity sub-cases are generated from selected heavy gate identities. + switch q_native_idx + case 0 { + { + let var0 := 0x1 + let a_0 := mload(0x8520) + let a_0_next_1 := mload(0x85c0) + let var1 := mulmod(a_0, a_0_next_1, r) + let var2 := 0x100000000000000 + let a_1_next_1 := mload(0x85e0) + let var3 := mulmod(a_0, a_1_next_1, r) + let var4 := mulmod(var2, var3, r) + let var5 := addmod(var1, var4, r) + let var6 := 0x10000000000000000000000000000 + let a_2_next_1 := mload(0x8600) + let var7 := mulmod(a_0, a_2_next_1, r) + let var8 := mulmod(var6, var7, r) + let var9 := addmod(var5, var8, r) + let a_1 := mload(0x8540) + let var10 := mulmod(a_1, a_0_next_1, r) + let var11 := mulmod(var2, var10, r) + let var12 := addmod(var9, var11, r) + let var13 := mulmod(a_1, a_1_next_1, r) + let var14 := mulmod(var6, var13, r) + let var15 := addmod(var12, var14, r) + let var16 := 0x3212e00cde6d2002b119d800000347fcb8 + let a_6_next_1 := mload(0x87a0) + let var17 := mulmod(a_1, a_6_next_1, r) + let var18 := mulmod(var16, var17, r) + let var19 := addmod(var15, var18, r) + let a_2 := mload(0x8560) + let var20 := mulmod(a_2, a_0_next_1, r) + let var21 := mulmod(var6, var20, r) + let var22 := addmod(var19, var21, r) + let a_5_next_1 := mload(0x8780) + let var23 := mulmod(a_2, a_5_next_1, r) + let var24 := mulmod(var16, var23, r) + let var25 := addmod(var22, var24, r) + let var26 := 0x297784894e27525bc342b7fde37dba9366 + let var27 := mulmod(a_2, a_6_next_1, r) + let var28 := mulmod(var26, var27, r) + let var29 := addmod(var25, var28, r) + let a_3 := mload(0x8580) + let a_4_next_1 := mload(0x8760) + let var30 := mulmod(a_3, a_4_next_1, r) + let var31 := mulmod(var16, var30, r) + let var32 := addmod(var29, var31, r) + let var33 := mulmod(a_3, a_5_next_1, r) + let var34 := mulmod(var26, var33, r) + let var35 := addmod(var32, var34, r) + let var36 := 0x340f2ebe380a0f5eff4360543988a61dc2 + let var37 := mulmod(a_3, a_6_next_1, r) + let var38 := mulmod(var36, var37, r) + let var39 := addmod(var35, var38, r) + let a_4 := mload(0x85a0) + let a_3_next_1 := mload(0x8740) + let var40 := mulmod(a_4, a_3_next_1, r) + let var41 := mulmod(var16, var40, r) + let var42 := addmod(var39, var41, r) + let var43 := mulmod(a_4, a_4_next_1, r) + let var44 := mulmod(var26, var43, r) + let var45 := addmod(var42, var44, r) + let var46 := mulmod(a_4, a_5_next_1, r) + let var47 := mulmod(var36, var46, r) + let var48 := addmod(var45, var47, r) + let var49 := 0x13af65741744bd7bb2c6872df2b800320 + let var50 := mulmod(a_4, a_6_next_1, r) + let var51 := mulmod(var49, var50, r) + let var52 := addmod(var48, var51, r) + let a_5 := mload(0x8620) + let var53 := mulmod(a_5, a_2_next_1, r) + let var54 := mulmod(var16, var53, r) + let var55 := addmod(var52, var54, r) + let var56 := mulmod(a_5, a_3_next_1, r) + let var57 := mulmod(var26, var56, r) + let var58 := addmod(var55, var57, r) + let var59 := mulmod(a_5, a_4_next_1, r) + let var60 := mulmod(var36, var59, r) + let var61 := addmod(var58, var60, r) + let var62 := mulmod(a_5, a_5_next_1, r) + let var63 := mulmod(var49, var62, r) + let var64 := addmod(var61, var63, r) + let var65 := 0x2cb9b546d20373eaf85e8f53db883cb548 + let var66 := mulmod(a_5, a_6_next_1, r) + let var67 := mulmod(var65, var66, r) + let var68 := addmod(var64, var67, r) + let a_6 := mload(0x8640) + let var69 := mulmod(a_6, a_1_next_1, r) + let var70 := mulmod(var16, var69, r) + let var71 := addmod(var68, var70, r) + let var72 := mulmod(a_6, a_2_next_1, r) + let var73 := mulmod(var26, var72, r) + let var74 := addmod(var71, var73, r) + let var75 := mulmod(a_6, a_3_next_1, r) + let var76 := mulmod(var36, var75, r) + let var77 := addmod(var74, var76, r) + let var78 := mulmod(a_6, a_4_next_1, r) + let var79 := mulmod(var49, var78, r) + let var80 := addmod(var77, var79, r) + let var81 := mulmod(a_6, a_5_next_1, r) + let var82 := mulmod(var65, var81, r) + let var83 := addmod(var80, var82, r) + let var84 := 0xc8557e86f90d0d89eed6eb5349a0f8820 + let var85 := mulmod(a_6, a_6_next_1, r) + let var86 := mulmod(var84, var85, r) + let var87 := addmod(var83, var86, r) + let var88 := mulmod(var2, a_1, r) + let var89 := addmod(a_0, var88, r) + let var90 := mulmod(var6, a_2, r) + let var91 := addmod(var89, var90, r) + let var92 := addmod(var87, var91, r) + let var93 := mulmod(var2, a_1_next_1, r) + let var94 := addmod(a_0_next_1, var93, r) + let var95 := mulmod(var6, a_2_next_1, r) + let var96 := addmod(var94, var95, r) + let var97 := addmod(var92, var96, r) + let a_7 := mload(0x8660) + let a_8 := mload(0x8680) + let var98 := mulmod(var2, a_8, r) + let var99 := addmod(a_7, var98, r) + let a_9 := mload(0x86a0) + let var100 := mulmod(var6, a_9, r) + let var101 := addmod(var99, var100, r) + let var102 := addmod(0, sub(r, var101), r) + let var103 := addmod(var97, var102, r) + let a_7_next_1 := mload(0x87c0) + let var104 := 0x241eabfffeb153ffffb9feffffffffaaab + let var105 := mulmod(a_7_next_1, var104, r) + let var106 := addmod(0, sub(r, var105), r) + let var107 := addmod(var103, var106, r) + let var108 := addmod(0, sub(r, var16), r) + let var109 := addmod(var107, var108, r) + let a_8_next_1 := mload(0x87e0) + let var110 := 0x73eda753299d7d483339d80809a1d80553b9202d7ffe85d4800008bb20000001 + let var111 := addmod(a_8_next_1, var110, r) + let var112 := 0x4000000000000000000000000000000000 + let var113 := mulmod(var111, var112, r) + let var114 := addmod(0, sub(r, var113), r) + let var115 := addmod(var109, var114, r) + let var116 := mulmod(var0, var115, r) + mstore(0xa960, var116) + } + mstore(0xa300, mulmod(mload(0xa300), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x60) + let q_selector_acc := mload(q_selector_ptr) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xa960), r)) + } + } + case 1 { + { + let var0 := 0x1 + let a_0 := mload(0x8520) + let var1 := 0x10000000000000000000000000000 + let var2 := addmod(a_0, var1, r) + let var3 := 0x100000000000000 + let a_1 := mload(0x8540) + let var4 := addmod(a_1, var1, r) + let var5 := mulmod(var3, var4, r) + let var6 := addmod(var2, var5, r) + let a_2 := mload(0x8560) + let var7 := addmod(a_2, var1, r) + let var8 := mulmod(var1, var7, r) + let var9 := addmod(var6, var8, r) + let a_7 := mload(0x8660) + let a_8 := mload(0x8680) + let var10 := mulmod(var3, a_8, r) + let var11 := addmod(a_7, var10, r) + let a_9 := mload(0x86a0) + let var12 := mulmod(var1, a_9, r) + let var13 := addmod(var11, var12, r) + let var14 := addmod(0, sub(r, var13), r) + let var15 := addmod(var9, var14, r) + let var16 := addmod(0, sub(r, var1), r) + let var17 := addmod(var15, var16, r) + let a_7_next_1 := mload(0x87c0) + let var18 := 0x241eabfffeb153ffffb9feffffffffaaab + let var19 := mulmod(a_7_next_1, var18, r) + let var20 := addmod(0, sub(r, var19), r) + let var21 := addmod(var17, var20, r) + let var22 := 0xd9d44a30b019261257667fde3844a8cd6 + let var23 := addmod(0, sub(r, var22), r) + let var24 := addmod(var21, var23, r) + let a_8_next_1 := mload(0x87e0) + let var25 := 0x73eda753299d7d483339d80809a1d80553bda402fffe5b6e855000003ab00002 + let var26 := addmod(a_8_next_1, var25, r) + let var27 := 0x4000000000000000000000000000000000 + let var28 := mulmod(var26, var27, r) + let var29 := addmod(0, sub(r, var28), r) + let var30 := addmod(var24, var29, r) + let var31 := mulmod(var0, var30, r) + mstore(0xa960, var31) + } + mstore(0xa300, mulmod(mload(0xa300), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x80) + let q_selector_acc := mload(q_selector_ptr) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xa960), r)) + } + } + case 2 { + { + let var0 := 0x1 + let f_0 := mload(0x8ae0) + let a_0_next_1 := mload(0x85c0) + let var1 := addmod(0, sub(r, a_0_next_1), r) + let var2 := addmod(f_0, var1, r) + let var3 := 0x1b8114c381b922fd5d6d241210e2d8a68ad5744053ba9e776118de4107b51ace + let a_0 := mload(0x8520) + let var4 := mulmod(a_0, a_0, r) + let a_3 := mload(0x8580) + let var5 := mulmod(var4, a_3, r) + let var6 := mulmod(var3, var5, r) + let var7 := addmod(var2, var6, r) + let var8 := 0x3df32e4cc4cb2ed20e5d21899cf5331775990ccaec4c09b4e3717213fcc0d763 + let a_1 := mload(0x8540) + let var9 := mulmod(a_1, a_1, r) + let a_4 := mload(0x85a0) + let var10 := mulmod(var9, a_4, r) + let var11 := mulmod(var8, var10, r) + let var12 := addmod(var7, var11, r) + let var13 := 0x3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc1 + let a_2 := mload(0x8560) + let var14 := mulmod(a_2, a_2, r) + let a_5 := mload(0x8620) + let var15 := mulmod(var14, a_5, r) + let var16 := mulmod(var13, var15, r) + let var17 := addmod(var12, var16, r) + let var18 := mulmod(var0, var17, r) + mstore(0xa960, var18) + } + mstore(0xa300, mulmod(mload(0xa300), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x120) + let q_selector_acc := mload(q_selector_ptr) + q_selector_acc := mulmod(q_selector_acc, mload(add(0xa340, 0x20)), r) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xa960), r)) + } + } + case 3 { + { + let var0 := 0x1 + let f_1 := mload(0x8b00) + let a_1_next_1 := mload(0x85e0) + let var1 := addmod(0, sub(r, a_1_next_1), r) + let var2 := addmod(f_1, var1, r) + let var3 := 0x404d21073985d14e432a4ad76d3fae06ca74314b950fe7b1d7f501cd31a8b374 + let a_0 := mload(0x8520) + let var4 := mulmod(a_0, a_0, r) + let a_3 := mload(0x8580) + let var5 := mulmod(var4, a_3, r) + let var6 := mulmod(var3, var5, r) + let var7 := addmod(var2, var6, r) + let var8 := 0xb2cc8704264c6bd81bc620e9e524d4b73e9b2317679422ff7fa1603955649f1 + let a_1 := mload(0x8540) + let var9 := mulmod(a_1, a_1, r) + let a_4 := mload(0x85a0) + let var10 := mulmod(var9, a_4, r) + let var11 := mulmod(var8, var10, r) + let var12 := addmod(var7, var11, r) + let var13 := 0xfdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f + let a_2 := mload(0x8560) + let var14 := mulmod(a_2, a_2, r) + let a_5 := mload(0x8620) + let var15 := mulmod(var14, a_5, r) + let var16 := mulmod(var13, var15, r) + let var17 := addmod(var12, var16, r) + let var18 := mulmod(var0, var17, r) + mstore(0xa960, var18) + } + mstore(0xa300, mulmod(mload(0xa300), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x120) + let q_selector_acc := mload(q_selector_ptr) + q_selector_acc := mulmod(q_selector_acc, mload(add(0xa340, 0x20)), r) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xa960), r)) + } + } + default { revert(0, 0) } + } + // VM 0x0b FOLD_SELECTOR: consume q_top into one simple-selector bucket. + case 0x0b { + // Operand layout packed into three bytes: + // high byte: selector bucket index; + // low u16 : y-power gap since this selector's + // previous contribution. + let q_selector_payload := shr(232, mload(q_pc)) + q_pc := add(q_pc, 3) + let q_sel_idx := shr(16, q_selector_payload) + let q_sel_gap := and(q_selector_payload, 0xffff) + let q_eval := q_top + q_has_top := 0 + // Simple-selector identity: keep the same y-batch + // position as main identities, then advance only this + // selector bucket by its codegen-known gap. + // + // The global fully-evaluated accumulator is still + // multiplied by y so later main identities land at the + // same y powers as Rust's reverse fold. + mstore(0xa300, mulmod(mload(0xa300), y, r)) + let q_target_ptr := add(SELECTOR_ACC_MPTR, shl(5, q_sel_idx)) + let q_sel_acc := mload(q_target_ptr) + if q_sel_gap { + // Selector buckets are sparse in the global + // identity stream. Precomputed y^gap advances only + // this selector's local accumulator. + q_sel_acc := mulmod(q_sel_acc, mload(add(0xa340, shl(5, q_sel_gap))), r) + } + mstore(q_target_ptr, addmod(q_sel_acc, q_eval, r)) + } + // Invalid generated bytecode should fail closed. 0x1a intentionally lands here. + default { + revert(0, 0) + } + } + + // Structured post-VM suffix. The current default uses this for + // regular trash constraints: it is smaller than fully unrolled + // Yul and cheaper than interpreting every trash operation. + // + // These generated blocks run after q_pc reaches q_end, but + // they still participate in the same identity order and write + // into the same numerator / selector accumulators. + { + let q_trash_tau := mload(TRASH_CHALLENGE_MPTR) + { + let f_0 := mload(0x8ae0) + let a_0_next_1 := mload(0x85c0) + let var0 := 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000 + let var1 := mulmod(a_0_next_1, var0, r) + let var2 := addmod(f_0, var1, r) + let var3 := 0x590ba402032e82eb1f660ef09796c5686345a5054ed96dae8e2d233633788771 + let a_0 := mload(0x8520) + let var4 := mulmod(var3, a_0, r) + let var5 := addmod(var2, var4, r) + let var6 := 0x52f789e4afc3801f7411102ee2f47cc5954a744e71cac98e75ea962a55a0a76f + let a_1 := mload(0x8540) + let var7 := mulmod(var6, a_1, r) + let var8 := addmod(var5, var7, r) + let var9 := 0x3509dd2fe3aac0080783557fec090fb1cb4b2b0901253c55282024331d1fe1a8 + let a_2 := mload(0x8560) + let var10 := q_pow5(a_2) + let var11 := mulmod(var9, var10, r) + let var12 := addmod(var8, var11, r) + let var13 := 0x333f8046ece5579cbd6872449c57f2703dfc8864cfadc06d587ff104a0d0c1f2 + let a_3 := mload(0x8580) + let var14 := q_pow5(a_3) + let var15 := mulmod(var13, var14, r) + let var16 := addmod(var12, var15, r) + let var17 := 0x412c98232b6ab8a47aa76ee814ef7ec6261987c9802f2cfc490e007951a60ca5 + let a_4 := mload(0x85a0) + let var18 := q_pow5(a_4) + let var19 := mulmod(var17, var18, r) + let var20 := addmod(var16, var19, r) + let var21 := 0x53fded36d490ba6b05a5d10fd99ffe5456baec6a6a8753199d5ebdc33c99790e + let a_5 := mload(0x8620) + let var22 := q_pow5(a_5) + let var23 := mulmod(var21, var22, r) + let var24 := addmod(var20, var23, r) + let var25 := 0x6ccb1c7d87f3c12a2bde4e68ac7f1e8b03481ba15d7f88f9a7f9b8310dd6d34 + let a_6 := mload(0x8640) + let var26 := q_pow5(a_6) + let var27 := mulmod(var25, var26, r) + let var28 := addmod(var24, var27, r) + let var29 := 0x3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc1 + let a_7 := mload(0x8660) + let var30 := q_pow5(a_7) + let var31 := mulmod(var29, var30, r) + let var32 := addmod(var28, var31, r) + let var33 := addmod(mulmod(0, q_trash_tau, r), var32, r) + let f_1 := mload(0x8b00) + let a_1_next_1 := mload(0x85e0) + let var34 := mulmod(a_1_next_1, var0, r) + let var35 := addmod(f_1, var34, r) + let var36 := 0x5b1fc262a28cbb8bf75d9b1a6edaa74591ec24cd9a209512213cec3a3c0f1a5d + let var37 := mulmod(var36, a_0, r) + let var38 := addmod(var35, var37, r) + let var39 := 0x4d0ea7f9c3fda06d9535b0fdafd8338bd47c2200b284fa71a325ff41ac358028 + let var40 := mulmod(var39, a_1, r) + let var41 := addmod(var38, var40, r) + let var42 := 0x26cc223e16f47c20e17cc6069605fa5a8af05ea4f6eb36029a641d23b818eb10 + let var43 := mulmod(var42, var10, r) + let var44 := addmod(var41, var43, r) + let var45 := 0x31e823a45e567484c1544e310c0fa5cd66547a8f0dde659ac61698c30e838d25 + let var46 := mulmod(var45, var14, r) + let var47 := addmod(var44, var46, r) + let var48 := 0x275a20361ea91992193920270d3e2d1f6361880ac0a439c64bef815d4469ba85 + let var49 := mulmod(var48, var18, r) + let var50 := addmod(var47, var49, r) + let var51 := 0x5f3a15bab4ce4097b1edc3a25002694b92395ce355a8a12fe557459d9633f701 + let var52 := mulmod(var51, var22, r) + let var53 := addmod(var50, var52, r) + let var54 := 0x301cf56f9b4577112cc4241cddf6484aaadedbf1bbd0f2351adf2e41c2fb2ecd + let var55 := mulmod(var54, var26, r) + let var56 := addmod(var53, var55, r) + let var57 := 0xfdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f + let var58 := mulmod(var57, var30, r) + let var59 := addmod(var56, var58, r) + let var60 := addmod(mulmod(var33, q_trash_tau, r), var59, r) + let f_2 := mload(0x8b20) + let var61 := mulmod(a_3, var0, r) + let var62 := addmod(f_2, var61, r) + let var63 := 0x5e1d3dbecda6214343e24a47f45c5d033197ad01b65a730af95dc57e90c49140 + let var64 := mulmod(var63, a_0, r) + let var65 := addmod(var62, var64, r) + let var66 := 0x6bd72f9cfc53af9d931896e77ea5c61244cb6d5fae8954f37dc7b9002f5aa78a + let var67 := mulmod(var66, a_1, r) + let var68 := addmod(var65, var67, r) + let var69 := 0x4997c5aa3a5fa07bcaf880a9054bef831effbd9cd58e46d9bb4fb88ef99de0db + let var70 := mulmod(var69, var10, r) + let var71 := addmod(var68, var70, r) + let var72 := addmod(mulmod(var60, q_trash_tau, r), var71, r) + let f_3 := mload(0x8b40) + let var73 := mulmod(a_4, var0, r) + let var74 := addmod(f_3, var73, r) + let var75 := 0x222e83e70453dfee19b402e9fa8dfe2c4987b034d0be3ceb478b3022e97934c1 + let var76 := mulmod(var75, a_0, r) + let var77 := addmod(var74, var76, r) + let var78 := 0x26c2cc87f95726b28f33ca03409a460ec987cfe12adae32769e3565865d07191 + let var79 := mulmod(var78, a_1, r) + let var80 := addmod(var77, var79, r) + let var81 := 0x4382d0938a760120dd6cef8f3b90a0c38abae475e3d21e39365472b76d780272 + let var82 := mulmod(var81, var10, r) + let var83 := addmod(var80, var82, r) + let var84 := mulmod(var69, var14, r) + let var85 := addmod(var83, var84, r) + let var86 := addmod(mulmod(var72, q_trash_tau, r), var85, r) + let f_4 := mload(0x8a40) + let var87 := mulmod(a_5, var0, r) + let var88 := addmod(f_4, var87, r) + let var89 := 0x726df1506749848155630b86ae25a82b281ecd050fe3a52d85a181fa87202e4b + let var90 := mulmod(var89, a_0, r) + let var91 := addmod(var88, var90, r) + let var92 := 0x24822e1af9aa2887c912c87eb0f20bd332330e7e55cd784de67cb407a9f05520 + let var93 := mulmod(var92, a_1, r) + let var94 := addmod(var91, var93, r) + let var95 := 0x4e5280109d8f96b8bfb543a6b1af25fb56a9db616af85a90eedc558e3eb1ea29 + let var96 := mulmod(var95, var10, r) + let var97 := addmod(var94, var96, r) + let var98 := mulmod(var81, var14, r) + let var99 := addmod(var97, var98, r) + let var100 := mulmod(var69, var18, r) + let var101 := addmod(var99, var100, r) + let var102 := addmod(mulmod(var86, q_trash_tau, r), var101, r) + let f_5 := mload(0x8a60) + let var103 := mulmod(a_6, var0, r) + let var104 := addmod(f_5, var103, r) + let var105 := 0x2f5908b169c6cf1bd26dcf0f9e5105481f5164f3ece0582bf3098312167751a7 + let var106 := mulmod(var105, a_0, r) + let var107 := addmod(var104, var106, r) + let var108 := 0x23a6684b942d726a22e4d5b8d8ff83aeaa773f62600184efe5d033d7c7c6e827 + let var109 := mulmod(var108, a_1, r) + let var110 := addmod(var107, var109, r) + let var111 := 0x1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e + let var112 := mulmod(var111, var10, r) + let var113 := addmod(var110, var112, r) + let var114 := mulmod(var95, var14, r) + let var115 := addmod(var113, var114, r) + let var116 := mulmod(var81, var18, r) + let var117 := addmod(var115, var116, r) + let var118 := mulmod(var69, var22, r) + let var119 := addmod(var117, var118, r) + let var120 := addmod(mulmod(var102, q_trash_tau, r), var119, r) + let f_6 := mload(0x8a80) + let var121 := mulmod(a_7, var0, r) + let var122 := addmod(f_6, var121, r) + let var123 := 0x6d05a41959f539a7fc9ec0972ea1e3dbb6fc67dd51daf3414f7fbbb091c7274a + let var124 := mulmod(var123, a_0, r) + let var125 := addmod(var122, var124, r) + let var126 := 0x27e7119226c42a6d19c1541904b99ae40685511ed2e078964b74594d38340849 + let var127 := mulmod(var126, a_1, r) + let var128 := addmod(var125, var127, r) + let var129 := 0xd94c46a8456352aa44d7a885ab59e3a36664e6fb25e826f8a4cd79822f0533 + let var130 := mulmod(var129, var10, r) + let var131 := addmod(var128, var130, r) + let var132 := mulmod(var111, var14, r) + let var133 := addmod(var131, var132, r) + let var134 := mulmod(var95, var18, r) + let var135 := addmod(var133, var134, r) + let var136 := mulmod(var81, var22, r) + let var137 := addmod(var135, var136, r) + let var138 := mulmod(var69, var26, r) + let var139 := addmod(var137, var138, r) + let var140 := addmod(mulmod(var120, q_trash_tau, r), var139, r) + let f_7 := mload(0x8aa0) + let a_2_next_1 := mload(0x8600) + let var141 := mulmod(a_2_next_1, var0, r) + let var142 := addmod(f_7, var141, r) + let var143 := 0x70d8f2a733a64d650faccc9b1c2a766a9544bb3ff1a11ee73cb43947ef386633 + let var144 := mulmod(var143, a_0, r) + let var145 := addmod(var142, var144, r) + let var146 := 0x40fa389feb2522bb934881ac9ed749aee2296502af592418c6b5675c0f560261 + let var147 := mulmod(var146, a_1, r) + let var148 := addmod(var145, var147, r) + let var149 := 0x1f61345b652161410c5e29f51e301ae56342af824bc110649393d2b911c50d3e + let var150 := mulmod(var149, var10, r) + let var151 := addmod(var148, var150, r) + let var152 := mulmod(var129, var14, r) + let var153 := addmod(var151, var152, r) + let var154 := mulmod(var111, var18, r) + let var155 := addmod(var153, var154, r) + let var156 := mulmod(var95, var22, r) + let var157 := addmod(var155, var156, r) + let var158 := mulmod(var81, var26, r) + let var159 := addmod(var157, var158, r) + let var160 := mulmod(var69, var30, r) + let var161 := addmod(var159, var160, r) + let var162 := addmod(mulmod(var140, q_trash_tau, r), var161, r) + let f_26 := mload(0x8c20) + let q_trash_one_minus_selector := addmod(1, sub(r, f_26), r) + let q_trash_scaled := mulmod(q_trash_one_minus_selector, mload(0x91a0), r) + let q_trash_eval := addmod(var162, sub(r, q_trash_scaled), r) + mstore(0xa300, mulmod(mload(0xa300), y, r)) + mstore(0xa300, addmod(mload(0xa300), q_trash_eval, r)) + } + } + // Finish selector buckets by applying the codegen-known tail + // from each selector's last identity to the end of the global + // y-batch. + // + // After this step, every selector bucket is aligned with the + // final global y position and can be multiplied by its fixed + // selector commitment in the linearized MSM. + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x00) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xa340, 0x0600)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x20) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xa340, 0x05e0)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x40) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xa340, 0x0580)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x60) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xa340, 0x0520)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x80) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xa340, 0x04c0)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0xa0) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xa340, 0x0460)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0xc0) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xa340, 0x0400)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0xe0) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xa340, 0x03a0)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x0100) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xa340, 0x0340)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x0120) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xa340, 0x0280)), r)) + } + + // Fully evaluated identities are the constant-polynomial side + // of the linearization query. Rust subtracts that grouped + // scalar into expected_eval, so Solidity stores -nu_y(x). + let linearization_expected_eval := addmod(0, sub(r, mload(0xa300)), r) + mstore(QUOTIENT_EVAL_MPTR, linearization_expected_eval) + pop(y) + } + gas_checkpoint(12) // after batched identity numerator reconstruction + + // =============================================================== + // Prepare linearization scalars for the final PCS MSM. + // + // The linearized commitment is + // (1 - x^n) * Σ_i x_split^i * Q_i + // + Σ_j sel_acc_j * S_j_com, + // where x_split = x^(n-1). Instead of materializing that point + // with a standalone G1MSM here, PCS block 5 expands the + // linearized commitment into its quotient and selector + // pairs inside the already-fused final MSM. + // + // QUOTIENT_MPTR is no longer a G1 point in this path. Its first + // two words carry: + // word 0: x_split + // word 1: one_minus_x_n + // =============================================================== + { + let x := mload(X_MPTR) + let k := 20 + // Compute both x^n and x^(n-1) with the same squaring walk: + // x_pow_2i tracks x^(2^i), while x_pow_2i_minus1 tracks + // x^(2^i - 1). + let x_pow_2i := x + let x_pow_2i_minus1 := 1 + for { let idx := 0 } lt(idx, k) { idx := add(idx, 1) } { + x_pow_2i_minus1 := mulmod( + mulmod(x_pow_2i_minus1, x_pow_2i_minus1, r), + x, + r + ) + x_pow_2i := mulmod(x_pow_2i, x_pow_2i, r) + } + let x_split := x_pow_2i_minus1 + let one_minus_x_n := addmod(1, sub(r, x_pow_2i), r) + + // PCS block 5 interprets this 2-word payload as scalar + // metadata, not as a materialized G1 point. + mstore(QUOTIENT_MPTR, x_split) + mstore(add(QUOTIENT_MPTR, 0x20), one_minus_x_n) + } + gas_checkpoint(13) // after linearization scalar prep + + // =============================================================== + // PCS computation (multi-prepare emitter from Step 5). + // + // The Rust lowering stage has already expanded the KZG multi-open + // equation into a sequence of generated Yul sub-blocks. Those + // blocks populate: + // - F_EVAL_MPTR / V_MPTR scalar batching values; + // - FINAL_COM_MPTR for the fused commitment MSM; + // - PAIRING_LHS_MPTR and PAIRING_RHS_MPTR for the final pairing. + // =============================================================== + { + // Generated PCS sub-block 1. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // 4 distinct rotation(s) + let x := mload(X_MPTR) + let omega := mload(OMEGA_MPTR) + let omega_inv := mload(OMEGA_INV_MPTR) + let x_pow_of_omega := x + mstore(add(ROT_POINTS_MPTR, 0x40), x_pow_of_omega) + x_pow_of_omega := mulmod(x_pow_of_omega, omega, r) + mstore(add(ROT_POINTS_MPTR, 0x60), x_pow_of_omega) + x_pow_of_omega := x + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + mstore(add(ROT_POINTS_MPTR, 0x20), x_pow_of_omega) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + mstore(add(ROT_POINTS_MPTR, 0x0), x_pow_of_omega) + } + gas_checkpoint(17) // after PCS sub-block 1 + // Generated PCS sub-block 2. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // pre-compute 43 x1 power(s) + let x1 := mload(X1_MPTR) + mstore(X1_POWERS_MPTR, 1) + let acc := 1 + let p := X1_POWERS_MPTR + for { let i := 0 } lt(i, 0x2a) { i := add(i, 1) } { + p := add(p, 0x20) + acc := mulmod(acc, x1, r) + mstore(p, and(acc, 0xffffffffffffffffffffffffffffffff)) + } + } + gas_checkpoint(18) // after PCS sub-block 2 + // Generated PCS sub-block 3. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // q_eval_set[0]: 43 commitment(s) (rolled, m>=4) + // stage per-(commit, rotation) eval source addresses + mstore(0xa300, 0x8a00) + mstore(0xa320, 0x8500) + mstore(0xa340, 0x90a0) + mstore(0xa360, 0x90c0) + mstore(0xa380, 0x9120) + mstore(0xa3a0, 0x9140) + mstore(0xa3c0, 0x91a0) + mstore(0xa3e0, 0x8a20) + mstore(0xa400, 0x8a40) + mstore(0xa420, 0x8a60) + mstore(0xa440, 0x8a80) + mstore(0xa460, 0x8aa0) + mstore(0xa480, 0x8ac0) + mstore(0xa4a0, 0x8ae0) + mstore(0xa4c0, 0x8b00) + mstore(0xa4e0, 0x8b20) + mstore(0xa500, 0x8b40) + mstore(0xa520, 0x8b60) + mstore(0xa540, 0x8b80) + mstore(0xa560, 0x8ba0) + mstore(0xa580, 0x8bc0) + mstore(0xa5a0, 0x8be0) + mstore(0xa5c0, 0x8c00) + mstore(0xa5e0, 0x8c20) + mstore(0xa600, 0x8c40) + mstore(0xa620, 0x8c60) + mstore(0xa640, 0x8c80) + mstore(0xa660, 0x8ca0) + mstore(0xa680, 0x8cc0) + mstore(0xa6a0, 0x8ce0) + mstore(0xa6c0, 0x8d00) + mstore(0xa6e0, 0x8d20) + mstore(0xa700, 0x8d40) + mstore(0xa720, 0x8d60) + mstore(0xa740, 0x8d80) + mstore(0xa760, 0x8da0) + mstore(0xa780, 0x8dc0) + mstore(0xa7a0, 0x8de0) + mstore(0xa7c0, 0x8e00) + mstore(0xa7e0, 0x8e20) + mstore(0xa800, 0x8e40) + mstore(0xa820, 0x8e60) + mstore(0xa840, QUOTIENT_EVAL_MPTR) + let q_eval_set_0 := mload(0x8a00) + let pow_p := add(X1_POWERS_MPTR, 0x20) + let eval_p := add(0xa300, 0x20) + for { let i := 1 } lt(i, 0x2b) { i := add(i, 1) } { + let pow := mload(pow_p) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(mload(eval_p)), pow, r), r) + pow_p := add(pow_p, 0x20) + eval_p := add(eval_p, 0x20) + } + mstore(add(Q_EVAL_SET_MPTR, 0x0), q_eval_set_0) + } + gas_checkpoint(19) // after PCS sub-block 3 + // Generated PCS sub-block 4. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // q_eval_set[1]: 3 commitment(s) + let q_eval_set_0 := mload(0x86e0) + let q_eval_set_1 := mload(0x89a0) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x8700), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x89c0), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x8720), mload(add(X1_POWERS_MPTR, 0x40)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x89e0), mload(add(X1_POWERS_MPTR, 0x40)), r), r) + mstore(add(Q_EVAL_SET_MPTR, 0x20), q_eval_set_0) + mstore(add(Q_EVAL_SET_MPTR, 0x40), q_eval_set_1) + } + gas_checkpoint(20) // after PCS sub-block 4 + // Generated PCS sub-block 5. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // q_eval_set[2]: 3 commitment(s) + let q_eval_set_0 := mload(0x9060) + let q_eval_set_1 := mload(0x9080) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x90e0), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x9100), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x9160), mload(add(X1_POWERS_MPTR, 0x40)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x9180), mload(add(X1_POWERS_MPTR, 0x40)), r), r) + mstore(add(Q_EVAL_SET_MPTR, 0x60), q_eval_set_0) + mstore(add(Q_EVAL_SET_MPTR, 0x80), q_eval_set_1) + } + gas_checkpoint(21) // after PCS sub-block 5 + // Generated PCS sub-block 6. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // q_eval_set[3]: 11 commitment(s) (rolled, m>=4) + // stage per-(commit, rotation) eval source addresses + mstore(0xa300, 0x8520) + mstore(0xa320, 0x85c0) + mstore(0xa340, 0x8840) + mstore(0xa360, 0x8540) + mstore(0xa380, 0x85e0) + mstore(0xa3a0, 0x8860) + mstore(0xa3c0, 0x8560) + mstore(0xa3e0, 0x8600) + mstore(0xa400, 0x8880) + mstore(0xa420, 0x8580) + mstore(0xa440, 0x8740) + mstore(0xa460, 0x88a0) + mstore(0xa480, 0x85a0) + mstore(0xa4a0, 0x8760) + mstore(0xa4c0, 0x88c0) + mstore(0xa4e0, 0x8620) + mstore(0xa500, 0x8780) + mstore(0xa520, 0x88e0) + mstore(0xa540, 0x8640) + mstore(0xa560, 0x87a0) + mstore(0xa580, 0x8900) + mstore(0xa5a0, 0x8660) + mstore(0xa5c0, 0x87c0) + mstore(0xa5e0, 0x8920) + mstore(0xa600, 0x8680) + mstore(0xa620, 0x87e0) + mstore(0xa640, 0x8940) + mstore(0xa660, 0x86a0) + mstore(0xa680, 0x8800) + mstore(0xa6a0, 0x8960) + mstore(0xa6c0, 0x86c0) + mstore(0xa6e0, 0x8820) + mstore(0xa700, 0x8980) + let q_eval_set_0 := mload(0x8520) + let q_eval_set_1 := mload(0x85c0) + let q_eval_set_2 := mload(0x8840) + let pow_p := add(X1_POWERS_MPTR, 0x20) + let eval_p := add(0xa300, 0x60) + for { let i := 1 } lt(i, 0xb) { i := add(i, 1) } { + let pow := mload(pow_p) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(mload(eval_p)), pow, r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(mload(add(eval_p, 0x20))), pow, r), r) + q_eval_set_2 := addmod(q_eval_set_2, mulmod(mload(mload(add(eval_p, 0x40))), pow, r), r) + pow_p := add(pow_p, 0x20) + eval_p := add(eval_p, 0x60) + } + mstore(add(Q_EVAL_SET_MPTR, 0xa0), q_eval_set_0) + mstore(add(Q_EVAL_SET_MPTR, 0xc0), q_eval_set_1) + mstore(add(Q_EVAL_SET_MPTR, 0xe0), q_eval_set_2) + } + gas_checkpoint(22) // after PCS sub-block 6 + // Generated PCS sub-block 7. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // q_eval_set[4]: 5 commitment(s) (rolled, m>=4) + // stage per-(commit, rotation) eval source addresses + mstore(0xa300, 0x8e80) + mstore(0xa320, 0x8ea0) + mstore(0xa340, 0x8ec0) + mstore(0xa360, 0x8ee0) + mstore(0xa380, 0x8f00) + mstore(0xa3a0, 0x8f20) + mstore(0xa3c0, 0x8f40) + mstore(0xa3e0, 0x8f60) + mstore(0xa400, 0x8f80) + mstore(0xa420, 0x8fa0) + mstore(0xa440, 0x8fc0) + mstore(0xa460, 0x8fe0) + mstore(0xa480, 0x9000) + mstore(0xa4a0, 0x9020) + mstore(0xa4c0, 0x9040) + let q_eval_set_0 := mload(0x8e80) + let q_eval_set_1 := mload(0x8ea0) + let q_eval_set_2 := mload(0x8ec0) + let pow_p := add(X1_POWERS_MPTR, 0x20) + let eval_p := add(0xa300, 0x60) + for { let i := 1 } lt(i, 0x5) { i := add(i, 1) } { + let pow := mload(pow_p) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(mload(eval_p)), pow, r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(mload(add(eval_p, 0x20))), pow, r), r) + q_eval_set_2 := addmod(q_eval_set_2, mulmod(mload(mload(add(eval_p, 0x40))), pow, r), r) + pow_p := add(pow_p, 0x20) + eval_p := add(eval_p, 0x60) + } + mstore(add(Q_EVAL_SET_MPTR, 0x100), q_eval_set_0) + mstore(add(Q_EVAL_SET_MPTR, 0x120), q_eval_set_1) + mstore(add(Q_EVAL_SET_MPTR, 0x140), q_eval_set_2) + } + gas_checkpoint(23) // after PCS sub-block 7 + // Generated PCS sub-block 8. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // f_eval via Horner over 5 reversed set(s) + let x2 := mload(X2_MPTR) + let x3 := mload(X3_MPTR) + let f_eval := 0 + let Q_EVAL_CPTR := mload(Q_EVAL_CPTR_MPTR) + let rot_pt_0 := mload(add(ROT_POINTS_MPTR, 0x0)) + let rot_pt_1 := mload(add(ROT_POINTS_MPTR, 0x20)) + let rot_pt_2 := mload(add(ROT_POINTS_MPTR, 0x40)) + let rot_pt_3 := mload(add(ROT_POINTS_MPTR, 0x60)) + // --- set 4 (cardinality 3) --- + { + let dx_0 := addmod(x3, sub(r, rot_pt_2), r) + let dx_1 := addmod(x3, sub(r, rot_pt_3), r) + let dx_2 := addmod(x3, sub(r, rot_pt_0), r) + let lbasis_0 := 1 + lbasis_0 := mulmod(lbasis_0, addmod(rot_pt_2, sub(r, rot_pt_3), r), r) + lbasis_0 := mulmod(lbasis_0, addmod(rot_pt_2, sub(r, rot_pt_0), r), r) + let lbasis_1 := 1 + lbasis_1 := mulmod(lbasis_1, addmod(rot_pt_3, sub(r, rot_pt_2), r), r) + lbasis_1 := mulmod(lbasis_1, addmod(rot_pt_3, sub(r, rot_pt_0), r), r) + let lbasis_2 := 1 + lbasis_2 := mulmod(lbasis_2, addmod(rot_pt_0, sub(r, rot_pt_2), r), r) + lbasis_2 := mulmod(lbasis_2, addmod(rot_pt_0, sub(r, rot_pt_3), r), r) + let bp_0 := dx_0 + let bp_1 := mulmod(bp_0, dx_1, r) + let bp_2 := mulmod(bp_1, dx_2, r) + let bp_3 := mulmod(bp_2, lbasis_0, r) + let bp_4 := mulmod(bp_3, lbasis_1, r) + let bp_5 := mulmod(bp_4, lbasis_2, r) + let bq := scalar_inv(bp_5) + let lbasis_inv_2 := mulmod(bq, bp_4, r) + bq := mulmod(bq, lbasis_2, r) + let lbasis_inv_1 := mulmod(bq, bp_3, r) + bq := mulmod(bq, lbasis_1, r) + let lbasis_inv_0 := mulmod(bq, bp_2, r) + bq := mulmod(bq, lbasis_0, r) + let dx_inv_2 := mulmod(bq, bp_1, r) + bq := mulmod(bq, dx_2, r) + let dx_inv_1 := mulmod(bq, bp_0, r) + bq := mulmod(bq, dx_1, r) + let dx_inv_0 := bq + let den_inv := dx_inv_0 + den_inv := mulmod(den_inv, dx_inv_1, r) + den_inv := mulmod(den_inv, dx_inv_2, r) + let eval := mulmod(calldataload(add(Q_EVAL_CPTR, 0x80)), den_inv, r) + let term_0 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0x100)), dx_inv_0, r), lbasis_inv_0, r) + eval := addmod(eval, sub(r, term_0), r) + let term_1 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0x120)), dx_inv_1, r), lbasis_inv_1, r) + eval := addmod(eval, sub(r, term_1), r) + let term_2 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0x140)), dx_inv_2, r), lbasis_inv_2, r) + eval := addmod(eval, sub(r, term_2), r) + f_eval := addmod(mulmod(f_eval, x2, r), eval, r) + } + // --- set 3 (cardinality 3) --- + { + let dx_0 := addmod(x3, sub(r, rot_pt_2), r) + let dx_1 := addmod(x3, sub(r, rot_pt_3), r) + let dx_2 := addmod(x3, sub(r, rot_pt_1), r) + let lbasis_0 := 1 + lbasis_0 := mulmod(lbasis_0, addmod(rot_pt_2, sub(r, rot_pt_3), r), r) + lbasis_0 := mulmod(lbasis_0, addmod(rot_pt_2, sub(r, rot_pt_1), r), r) + let lbasis_1 := 1 + lbasis_1 := mulmod(lbasis_1, addmod(rot_pt_3, sub(r, rot_pt_2), r), r) + lbasis_1 := mulmod(lbasis_1, addmod(rot_pt_3, sub(r, rot_pt_1), r), r) + let lbasis_2 := 1 + lbasis_2 := mulmod(lbasis_2, addmod(rot_pt_1, sub(r, rot_pt_2), r), r) + lbasis_2 := mulmod(lbasis_2, addmod(rot_pt_1, sub(r, rot_pt_3), r), r) + let bp_0 := dx_0 + let bp_1 := mulmod(bp_0, dx_1, r) + let bp_2 := mulmod(bp_1, dx_2, r) + let bp_3 := mulmod(bp_2, lbasis_0, r) + let bp_4 := mulmod(bp_3, lbasis_1, r) + let bp_5 := mulmod(bp_4, lbasis_2, r) + let bq := scalar_inv(bp_5) + let lbasis_inv_2 := mulmod(bq, bp_4, r) + bq := mulmod(bq, lbasis_2, r) + let lbasis_inv_1 := mulmod(bq, bp_3, r) + bq := mulmod(bq, lbasis_1, r) + let lbasis_inv_0 := mulmod(bq, bp_2, r) + bq := mulmod(bq, lbasis_0, r) + let dx_inv_2 := mulmod(bq, bp_1, r) + bq := mulmod(bq, dx_2, r) + let dx_inv_1 := mulmod(bq, bp_0, r) + bq := mulmod(bq, dx_1, r) + let dx_inv_0 := bq + let den_inv := dx_inv_0 + den_inv := mulmod(den_inv, dx_inv_1, r) + den_inv := mulmod(den_inv, dx_inv_2, r) + let eval := mulmod(calldataload(add(Q_EVAL_CPTR, 0x60)), den_inv, r) + let term_0 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0xa0)), dx_inv_0, r), lbasis_inv_0, r) + eval := addmod(eval, sub(r, term_0), r) + let term_1 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0xc0)), dx_inv_1, r), lbasis_inv_1, r) + eval := addmod(eval, sub(r, term_1), r) + let term_2 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0xe0)), dx_inv_2, r), lbasis_inv_2, r) + eval := addmod(eval, sub(r, term_2), r) + f_eval := addmod(mulmod(f_eval, x2, r), eval, r) + } + // --- set 2 (cardinality 2) --- + { + let dx_0 := addmod(x3, sub(r, rot_pt_2), r) + let dx_1 := addmod(x3, sub(r, rot_pt_3), r) + let lbasis_0 := 1 + lbasis_0 := mulmod(lbasis_0, addmod(rot_pt_2, sub(r, rot_pt_3), r), r) + let lbasis_1 := 1 + lbasis_1 := mulmod(lbasis_1, addmod(rot_pt_3, sub(r, rot_pt_2), r), r) + let bp_0 := dx_0 + let bp_1 := mulmod(bp_0, dx_1, r) + let bp_2 := mulmod(bp_1, lbasis_0, r) + let bp_3 := mulmod(bp_2, lbasis_1, r) + let bq := scalar_inv(bp_3) + let lbasis_inv_1 := mulmod(bq, bp_2, r) + bq := mulmod(bq, lbasis_1, r) + let lbasis_inv_0 := mulmod(bq, bp_1, r) + bq := mulmod(bq, lbasis_0, r) + let dx_inv_1 := mulmod(bq, bp_0, r) + bq := mulmod(bq, dx_1, r) + let dx_inv_0 := bq + let den_inv := dx_inv_0 + den_inv := mulmod(den_inv, dx_inv_1, r) + let eval := mulmod(calldataload(add(Q_EVAL_CPTR, 0x40)), den_inv, r) + let term_0 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0x60)), dx_inv_0, r), lbasis_inv_0, r) + eval := addmod(eval, sub(r, term_0), r) + let term_1 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0x80)), dx_inv_1, r), lbasis_inv_1, r) + eval := addmod(eval, sub(r, term_1), r) + f_eval := addmod(mulmod(f_eval, x2, r), eval, r) + } + // --- set 1 (cardinality 2) --- + { + let dx_0 := addmod(x3, sub(r, rot_pt_2), r) + let dx_1 := addmod(x3, sub(r, rot_pt_1), r) + let lbasis_0 := 1 + lbasis_0 := mulmod(lbasis_0, addmod(rot_pt_2, sub(r, rot_pt_1), r), r) + let lbasis_1 := 1 + lbasis_1 := mulmod(lbasis_1, addmod(rot_pt_1, sub(r, rot_pt_2), r), r) + let bp_0 := dx_0 + let bp_1 := mulmod(bp_0, dx_1, r) + let bp_2 := mulmod(bp_1, lbasis_0, r) + let bp_3 := mulmod(bp_2, lbasis_1, r) + let bq := scalar_inv(bp_3) + let lbasis_inv_1 := mulmod(bq, bp_2, r) + bq := mulmod(bq, lbasis_1, r) + let lbasis_inv_0 := mulmod(bq, bp_1, r) + bq := mulmod(bq, lbasis_0, r) + let dx_inv_1 := mulmod(bq, bp_0, r) + bq := mulmod(bq, dx_1, r) + let dx_inv_0 := bq + let den_inv := dx_inv_0 + den_inv := mulmod(den_inv, dx_inv_1, r) + let eval := mulmod(calldataload(add(Q_EVAL_CPTR, 0x20)), den_inv, r) + let term_0 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0x20)), dx_inv_0, r), lbasis_inv_0, r) + eval := addmod(eval, sub(r, term_0), r) + let term_1 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0x40)), dx_inv_1, r), lbasis_inv_1, r) + eval := addmod(eval, sub(r, term_1), r) + f_eval := addmod(mulmod(f_eval, x2, r), eval, r) + } + // --- set 0 (cardinality 1) --- + { + let dx0 := addmod(x3, sub(r, rot_pt_2), r) + let dx0_inv := scalar_inv(dx0) + let eval := mulmod(addmod(calldataload(add(Q_EVAL_CPTR, 0x0)), sub(r, mload(add(Q_EVAL_SET_MPTR, 0x0))), r), dx0_inv, r) + f_eval := addmod(mulmod(f_eval, x2, r), eval, r) + } + mstore(F_EVAL_MPTR, f_eval) + } + gas_checkpoint(24) // after PCS sub-block 8 + // Generated PCS sub-block 9. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // build final_com and v (KZG single-opening proof, fused MSM) + // final MSM input length from circuit/VK shape: 78 term(s) + let x4 := mload(X4_MPTR) + let lin_x_split := mload(QUOTIENT_MPTR) + let lin_one_minus_x_n := mload(add(QUOTIENT_MPTR, 0x20)) + let Q_EVAL_CPTR := mload(Q_EVAL_CPTR_MPTR) + let x4_pow_full := 1 + x4_pow_full := mulmod(x4_pow_full, x4, r) + let x4_pow_1 := and(x4_pow_full, 0xffffffffffffffffffffffffffffffff) + x4_pow_full := mulmod(x4_pow_full, x4, r) + let x4_pow_2 := and(x4_pow_full, 0xffffffffffffffffffffffffffffffff) + x4_pow_full := mulmod(x4_pow_full, x4, r) + let x4_pow_3 := and(x4_pow_full, 0xffffffffffffffffffffffffffffffff) + x4_pow_full := mulmod(x4_pow_full, x4, r) + let x4_pow_4 := and(x4_pow_full, 0xffffffffffffffffffffffffffffffff) + x4_pow_full := mulmod(x4_pow_full, x4, r) + let x4_pow_5 := and(x4_pow_full, 0xffffffffffffffffffffffffffffffff) + let v := calldataload(Q_EVAL_CPTR) + v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x20)), x4_pow_1, r), r) + v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x40)), x4_pow_2, r), r) + v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x60)), x4_pow_3, r), r) + v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x80)), x4_pow_4, r), r) + v := addmod(v, mulmod(mload(F_EVAL_MPTR), x4_pow_5, r), r) + mcopy(0xa300, 0x98c0, 0x80) + mstore(0xa380, 1) + mcopy(0xa3a0, 0x9940, 0x80) + mstore(0xa420, mload(add(X1_POWERS_MPTR, 0x40))) + mcopy(0xa440, 0x9d40, 0x80) + mstore(0xa4c0, mload(add(X1_POWERS_MPTR, 0x60))) + mcopy(0xa4e0, 0x99c0, 0x80) + mstore(0xa560, mload(add(X1_POWERS_MPTR, 0x80))) + mcopy(0xa580, 0x9dc0, 0x80) + mstore(0xa600, mload(add(X1_POWERS_MPTR, 0xa0))) + mcopy(0xa620, 0x9f40, 0x80) + mstore(0xa6a0, mload(add(X1_POWERS_MPTR, 0xc0))) + mcopy(0xa6c0, 0x5780, 0x80) + mstore(0xa740, mload(add(X1_POWERS_MPTR, 0xe0))) + mcopy(0xa760, 0x5500, 0x80) + mstore(0xa7e0, mload(add(X1_POWERS_MPTR, 0x100))) + mcopy(0xa800, 0x5580, 0x80) + mstore(0xa880, mload(add(X1_POWERS_MPTR, 0x120))) + mcopy(0xa8a0, 0x5600, 0x80) + mstore(0xa920, mload(add(X1_POWERS_MPTR, 0x140))) + mcopy(0xa940, 0x5680, 0x80) + mstore(0xa9c0, mload(add(X1_POWERS_MPTR, 0x160))) + mcopy(0xa9e0, 0x5700, 0x80) + mstore(0xaa60, mload(add(X1_POWERS_MPTR, 0x180))) + mcopy(0xaa80, 0x5300, 0x80) + mstore(0xab00, mload(add(X1_POWERS_MPTR, 0x1a0))) + mcopy(0xab20, 0x5380, 0x80) + mstore(0xaba0, mload(add(X1_POWERS_MPTR, 0x1c0))) + mcopy(0xabc0, 0x5400, 0x80) + mstore(0xac40, mload(add(X1_POWERS_MPTR, 0x1e0))) + mcopy(0xac60, 0x5480, 0x80) + mstore(0xace0, mload(add(X1_POWERS_MPTR, 0x200))) + mcopy(0xad00, 0x5800, 0x80) + mstore(0xad80, mload(add(X1_POWERS_MPTR, 0x220))) + mcopy(0xada0, 0x5880, 0x80) + mstore(0xae20, mload(add(X1_POWERS_MPTR, 0x240))) + mcopy(0xae40, 0x5900, 0x80) + mstore(0xaec0, mload(add(X1_POWERS_MPTR, 0x260))) + mcopy(0xaee0, 0x5980, 0x80) + mstore(0xaf60, mload(add(X1_POWERS_MPTR, 0x280))) + mcopy(0xaf80, 0x5b80, 0x80) + mstore(0xb000, mload(add(X1_POWERS_MPTR, 0x2a0))) + mcopy(0xb020, 0x5f00, 0x80) + mstore(0xb0a0, mload(add(X1_POWERS_MPTR, 0x2c0))) + mcopy(0xb0c0, 0x6000, 0x80) + mstore(0xb140, mload(add(X1_POWERS_MPTR, 0x2e0))) + mcopy(0xb160, 0x6080, 0x80) + mstore(0xb1e0, mload(add(X1_POWERS_MPTR, 0x300))) + mcopy(0xb200, 0x6100, 0x80) + mstore(0xb280, mload(add(X1_POWERS_MPTR, 0x320))) + mcopy(0xb2a0, 0x6180, 0x80) + mstore(0xb320, mload(add(X1_POWERS_MPTR, 0x340))) + mcopy(0xb340, 0x6200, 0x80) + mstore(0xb3c0, mload(add(X1_POWERS_MPTR, 0x360))) + mcopy(0xb3e0, 0x6280, 0x80) + mstore(0xb460, mload(add(X1_POWERS_MPTR, 0x380))) + mcopy(0xb480, 0x6300, 0x80) + mstore(0xb500, mload(add(X1_POWERS_MPTR, 0x3a0))) + mcopy(0xb520, 0x6380, 0x80) + mstore(0xb5a0, mload(add(X1_POWERS_MPTR, 0x3c0))) + mcopy(0xb5c0, 0x6400, 0x80) + mstore(0xb640, mload(add(X1_POWERS_MPTR, 0x3e0))) + mcopy(0xb660, 0x6480, 0x80) + mstore(0xb6e0, mload(add(X1_POWERS_MPTR, 0x400))) + mcopy(0xb700, 0x6500, 0x80) + mstore(0xb780, mload(add(X1_POWERS_MPTR, 0x420))) + mcopy(0xb7a0, 0x6580, 0x80) + mstore(0xb820, mload(add(X1_POWERS_MPTR, 0x440))) + mcopy(0xb840, 0x6600, 0x80) + mstore(0xb8c0, mload(add(X1_POWERS_MPTR, 0x460))) + mcopy(0xb8e0, 0x6680, 0x80) + mstore(0xb960, mload(add(X1_POWERS_MPTR, 0x480))) + mcopy(0xb980, 0x6700, 0x80) + mstore(0xba00, mload(add(X1_POWERS_MPTR, 0x4a0))) + mcopy(0xba20, 0x6780, 0x80) + mstore(0xbaa0, mload(add(X1_POWERS_MPTR, 0x4c0))) + mcopy(0xbac0, 0x6800, 0x80) + mstore(0xbb40, mload(add(X1_POWERS_MPTR, 0x4e0))) + mcopy(0xbb60, 0x6880, 0x80) + mstore(0xbbe0, mload(add(X1_POWERS_MPTR, 0x500))) + mcopy(0xbc00, 0x6900, 0x80) + mstore(0xbc80, mload(add(X1_POWERS_MPTR, 0x520))) + let lin_query_scalar_41 := mload(add(X1_POWERS_MPTR, 0x540)) + let lin_cur_scalar_41 := mulmod(lin_query_scalar_41, lin_one_minus_x_n, r) + mcopy(0xbca0, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x0), 0x80) + mstore(0xbd20, lin_cur_scalar_41) + lin_cur_scalar_41 := mulmod(lin_cur_scalar_41, lin_x_split, r) + mcopy(0xbd40, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x80), 0x80) + mstore(0xbdc0, lin_cur_scalar_41) + lin_cur_scalar_41 := mulmod(lin_cur_scalar_41, lin_x_split, r) + mcopy(0xbde0, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x100), 0x80) + mstore(0xbe60, lin_cur_scalar_41) + lin_cur_scalar_41 := mulmod(lin_cur_scalar_41, lin_x_split, r) + mcopy(0xbe80, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x180), 0x80) + mstore(0xbf00, lin_cur_scalar_41) + mcopy(0xbf20, 0x5a00, 0x80) + mstore(0xbfa0, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x0)), r)) + mcopy(0xbfc0, 0x5a80, 0x80) + mstore(0xc040, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x20)), r)) + mcopy(0xc060, 0x5b00, 0x80) + mstore(0xc0e0, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x40)), r)) + mcopy(0xc100, 0x5c00, 0x80) + mstore(0xc180, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x60)), r)) + mcopy(0xc1a0, 0x5c80, 0x80) + mstore(0xc220, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x80)), r)) + mcopy(0xc240, 0x5d00, 0x80) + mstore(0xc2c0, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0xa0)), r)) + mcopy(0xc2e0, 0x5d80, 0x80) + mstore(0xc360, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0xc0)), r)) + mcopy(0xc380, 0x5e00, 0x80) + mstore(0xc400, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0xe0)), r)) + mcopy(0xc420, 0x5e80, 0x80) + mstore(0xc4a0, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x100)), r)) + mcopy(0xc4c0, 0x5f80, 0x80) + mstore(0xc540, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x120)), r)) + mcopy(0xc560, 0x9740, 0x80) + mstore(0xc5e0, x4_pow_1) + mcopy(0xc600, 0x97c0, 0x80) + mstore(0xc680, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_1, r)) + mcopy(0xc6a0, 0x9840, 0x80) + mstore(0xc720, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_1, r)) + mcopy(0xc740, 0x9cc0, 0x80) + mstore(0xc7c0, x4_pow_2) + mcopy(0xc7e0, 0x9e40, 0x80) + mstore(0xc860, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_2, r)) + mcopy(0xc880, 0x9ec0, 0x80) + mstore(0xc900, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_2, r)) + mcopy(0xc920, 0x91c0, 0x80) + mstore(0xc9a0, x4_pow_3) + mcopy(0xc9c0, 0x9240, 0x80) + mstore(0xca40, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_3, r)) + mcopy(0xca60, 0x92c0, 0x80) + mstore(0xcae0, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_3, r)) + mcopy(0xcb00, 0x9340, 0x80) + mstore(0xcb80, mulmod(mload(add(X1_POWERS_MPTR, 0x60)), x4_pow_3, r)) + mcopy(0xcba0, 0x93c0, 0x80) + mstore(0xcc20, mulmod(mload(add(X1_POWERS_MPTR, 0x80)), x4_pow_3, r)) + mcopy(0xcc40, 0x9440, 0x80) + mstore(0xccc0, mulmod(mload(add(X1_POWERS_MPTR, 0xa0)), x4_pow_3, r)) + mcopy(0xcce0, 0x94c0, 0x80) + mstore(0xcd60, mulmod(mload(add(X1_POWERS_MPTR, 0xc0)), x4_pow_3, r)) + mcopy(0xcd80, 0x9540, 0x80) + mstore(0xce00, mulmod(mload(add(X1_POWERS_MPTR, 0xe0)), x4_pow_3, r)) + mcopy(0xce20, 0x95c0, 0x80) + mstore(0xcea0, mulmod(mload(add(X1_POWERS_MPTR, 0x100)), x4_pow_3, r)) + mcopy(0xcec0, 0x9640, 0x80) + mstore(0xcf40, mulmod(mload(add(X1_POWERS_MPTR, 0x120)), x4_pow_3, r)) + mcopy(0xcf60, 0x96c0, 0x80) + mstore(0xcfe0, mulmod(mload(add(X1_POWERS_MPTR, 0x140)), x4_pow_3, r)) + mcopy(0xd000, 0x9a40, 0x80) + mstore(0xd080, x4_pow_4) + mcopy(0xd0a0, 0x9ac0, 0x80) + mstore(0xd120, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_4, r)) + mcopy(0xd140, 0x9b40, 0x80) + mstore(0xd1c0, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_4, r)) + mcopy(0xd1e0, 0x9bc0, 0x80) + mstore(0xd260, mulmod(mload(add(X1_POWERS_MPTR, 0x60)), x4_pow_4, r)) + mcopy(0xd280, 0x9c40, 0x80) + mstore(0xd300, mulmod(mload(add(X1_POWERS_MPTR, 0x80)), x4_pow_4, r)) + mcopy(0xd320, F_COM_MPTR, 0x80) + mstore(0xd3a0, x4_pow_5) + if success { + success := staticcall(575096, 0x0c, 0xa300, 0x30c0, FINAL_COM_MPTR, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + mstore(V_MPTR, v) + } + gas_checkpoint(25) // after PCS sub-block 9 + // Generated PCS sub-block 10. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // Scale z*pi - vG before the final pairing check + // pairing inputs (LHS = pi; RHS = final_com - v*G + x3*pi) + mcopy(PAIRING_LHS_MPTR, PI_MPTR, 0x80) + mcopy(0x80, G1_BASE_MPTR, 0x80) + mstore(0x100, addmod(0, sub(r, mload(V_MPTR)), r)) + if success { + success := staticcall(62000, 0x0c, 0x80, 0xa0, 0x80, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + mcopy(0x100, FINAL_COM_MPTR, 0x80) + if success { + success := staticcall(50000, 0x0b, 0x80, 0x100, 0x80, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + mcopy(0x100, PI_MPTR, 0x80) + mstore(0x180, mload(X3_MPTR)) + if success { + success := staticcall(62000, 0x0c, 0x100, 0xa0, 0x100, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + if success { + success := staticcall(50000, 0x0b, 0x80, 0x100, 0x80, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + mcopy(PAIRING_RHS_MPTR, 0x80, 0x80) + } + } + gas_checkpoint(14) // after PCS computation block (= sub-block 6) + + // Batch the prevalidated public IVC accumulator pairing equation + // into the final KZG pairing. + // + // We do not simply multiply the two pairing equations together: + // two bad equations could cancel. Instead, after all four G1 + // pairing inputs are fixed, derive a verifier-local randomizer + // alpha and check: + // + // e(kzg_rhs + alpha * acc_rhs, G2_BASE) + // * e(kzg_lhs + alpha * acc_lhs, NEG_S_G2_BASE) == 1 + // + // If either original equation is bad, this combined equation + // holds for at most one alpha in Fr. + { + let batch_ptr := 0x0100 + + // Domain || KZG rhs/lhs || accumulator rhs/lhs. + mstore(batch_ptr, 0x70616972696e672d62617463682d6163632d6b7a670000000000000000) + mcopy(add(batch_ptr, 0x20), PAIRING_RHS_MPTR, 0x80) + mcopy(add(batch_ptr, 0xa0), PAIRING_LHS_MPTR, 0x80) + mcopy(add(batch_ptr, 0x0120), ACC_RHS_MPTR, 0x80) + mcopy(add(batch_ptr, 0x01a0), ACC_LHS_MPTR, 0x80) + // alpha is Fiat-Shamir over the fully materialized pairing + // inputs. Replace the negligible zero draw with one so the + // accumulator equation cannot be accidentally dropped. + let acc_pair_alpha := mod(keccak256(batch_ptr, 0x0220), r) + if iszero(acc_pair_alpha) { acc_pair_alpha := 1 } + + // PAIRING_RHS_MPTR += alpha * ACC_RHS_MPTR. + // First compute alpha * ACC_RHS with a one-pair G1MSM, then + // add it into the KZG RHS point. + mcopy(batch_ptr, ACC_RHS_MPTR, 0x80) + mstore(add(batch_ptr, 0x80), acc_pair_alpha) + if success { + success := staticcall(62000, 0x0c, batch_ptr, 0xa0, batch_ptr, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + mcopy(add(batch_ptr, 0x80), PAIRING_RHS_MPTR, 0x80) + if success { + success := staticcall(50000, 0x0b, batch_ptr, 0x0100, PAIRING_RHS_MPTR, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + + // PAIRING_LHS_MPTR += alpha * ACC_LHS_MPTR. + // Mirror the same randomized batching on the KZG LHS point. + mcopy(batch_ptr, ACC_LHS_MPTR, 0x80) + mstore(add(batch_ptr, 0x80), acc_pair_alpha) + if success { + success := staticcall(62000, 0x0c, batch_ptr, 0xa0, batch_ptr, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + mcopy(add(batch_ptr, 0x80), PAIRING_LHS_MPTR, 0x80) + if success { + success := staticcall(50000, 0x0b, batch_ptr, 0x0100, PAIRING_LHS_MPTR, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + } + gas_checkpoint(15) // after public accumulator pairing batch prep (omitted for no-accumulator VKs) + + // The Yul `ec_pairing` helper checks + // e(arg0, G2_BASE) * e(arg1, NEG_S_G2_BASE) == 1 + // i.e. e(arg0, [1]_2) = e(arg1, [s]_2). + // + // The KZG pairing identity is + // e(final_com - v*G + x3*pi, [1]_2) = e(pi, [s]_2), + // so arg0 must be (final_com - v*G + x3*pi) and arg1 must be + // pi. The PAIRING_*_MPTR slots store + // PAIRING_LHS_MPTR := pi + // PAIRING_RHS_MPTR := final_com - v*G + x3*pi + // -- the historical "LHS"/"RHS" naming follows the dual MSM + // accumulator (left = pi, right = combined) and *not* the + // pairing argument order. Pass them swapped to ec_pairing. + if iszero(success) { revert(0, 0) } + success := ec_pairing(success, PAIRING_RHS_MPTR, PAIRING_LHS_MPTR) + gas_checkpoint(16) // after final ec_pairing + + + + // Success path is terminal. Invalid inputs have already reverted, + // so the Solidity ABI observes `true`. + mstore(RETURN_MPTR, 1) + return(RETURN_MPTR, 0x20) + } + } +} \ No newline at end of file diff --git a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2VerifyingKey.runtime.bytecode.hex b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2VerifyingKey.runtime.bytecode.hex new file mode 100644 index 000000000..a67264b55 --- /dev/null +++ b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2VerifyingKey.runtime.bytecode.hex @@ -0,0 +1 @@ +0xfe56c0824fcff237dd8dc7b15f527346d9e1647d191815acb142500b0293e84f660000000000000000000000000000000000000000000000000000000000000013000000000000000000000000000000000000000000000000000000000000001473eda0144f284aae5b6554d46c21576b363d4ec725be2bff1a400fff0000100103e1c54bcb947035a57a6e07cb98de4a2f69e02d265e09d9fece7e0e39898d4b6c39442eade0092768ac033fa6f608750624a1bb17dbc026ef97c3573a28fc8c2a0ccbaa0613f093f2bb6e97859513f0b613d8587eaa92db9e5604b8d6b68d450000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000380000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4fcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e100000000000000000000000000000000024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb80000000000000000000000000000000013e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e000000000000000000000000000000000ce5d527727d6e118cc9cdc6da2e351aadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801000000000000000000000000000000000606c4a02ea734cc32acd2b02bc28b99cb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be000000000000000000000000000000000632aaf712568f19c297802268a7ad9dceea6ef7ab6f75a7d26781c8e90c7432bc5e99dcc219ba64010f3052123983ab00000000000000000000000000000000191ff4920e077a2f8cb3969ba8f05bc2aa9da8c95d640b1e051be7cf344ee7f01996df2568bf0e7ccd9eb709788200450000000000000000000000000000000005f434ebf45460a864ad5b17497c790371820c70c83aa186029536d22dff54373251152c28bc43269f95281eba1b012e0000000000000000000000000000000004d1c747141bcac15e77e3e1d3853254c8687afdad35345a04f79d9c2759007f6640676eb44aee7011ce5ad80744bb23000000000000000000000000000000000000000000000000000000000000000100bbe1fbe9ef1e2d62490b03a82bf9ef10f5e9b2323033669cf6c50481f63e0500000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000010000000000000000073eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff0000000073eda753299d7d483339d80809a1d80553bda402fffe5bfefeffffff0000000173eda753299d7d483339d80809a1d80553bca402fffe5bfeffffffff0000000173eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffb0000000173eda753299d7d483339d80809a1d80553bda402fbfe5bfeffffffff0000000173eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffefffff00173eda753299d7d483339d80809a1d80553bda402fffe5beeffffffff0000000100000000000000000000000000000010ff0726c5de281020ad8016cf6f6912130000000000000000000000000000002c64068790f917282347187665718b04c800000000000000000000000000000027241bb5338dce8a77499428839473bf3a0000000000000000000000000000002b7c4a26a1c7ae6fc4b499d04e4a463c4b000000000000000000000000000000274bc40fcf526be95333a8c22c794652980000000000000000000000000000002a5ee6db49930276e2939d1c43ac82f74473eda753299d7d483339d80809a1d7edd77e26c51c38afb5debf8afa00c15cc373eda753299d7d483339d80809a1d7c553bda402fffe5bfeffffffff0000000273eda753299d7d483339d80809a1d80553bda402fffe53ebc627fffef6280001000000000000000000000100000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000006bc66e553973f396854f5626172ba135587d41e37a68209402355093fdcaaf6c63f31e3f446953960c9d6964474300df43ab29179970f642a28e39d6c883c74b73eda753299d7d483339d70809a1d80553bda402fffe5bfeffffffff0000000173eda752299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001082738fdf02989b1adea81e1f27636cffb40621f85963b6afdcaaf6b023550950ffa8913e53429b2269c6ea3c25ed72610127aeb668d65bc5d71c628377c38b601ec1c0519185dfe86132d479c76d0786e1e037d0b05ca47648a1c5d29492c9b1e179025ca2470882b34e63940ccbd7ad9090bf414d43b696e093b5a8782528f4298bfee9a84c8ef8e83702075cb1abeb576f146636342e3db9ea6b0a4adf29d3c83b078e9abed278d042acc8f3bd21e228716f04be96af8025da860e2d1bba9427868260f487d1ef07edaadf37f5dbe705bd1318290f2577ae756b009c24f1103020e6a35e595abd22838beeadc45cfcb0545d85ca0ab2c59d44c203fac84a7000000000000000000000000000000000000000000000000d2010000000100000000000100001b7c3f8d3fe3c5b448f1bdeb2ae34698b72d6ce966fc208c05ed73eda753299d7d483339d80809a1d7fd4057a4c12f26d1c1778e3360a6820001057797fa7060856f215654ff11006fe0acf6a437e9477bf6f782dfac86f2cf7500000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000002000000000000000000000000000073eda753299d7d483339d80809a1d7e13511a4044eaa5bff4600ffff0000555673eda753299d7d483339d80809a1d7c553bda402fffe5bfeffffffff0000000173eda753299d7d483339d80809a1d7d340dd972492de594de627fffefcb8034973eda753299d7d483339d80809a1d7dbdc391ab4d8ac003bbd48021b82456c9b73eda753299d7d483339d80809a1d7d1448ee5caf5eefcffbc9fabc57759e23f73eda753299d7d483339d80809a1d80418c74cc18bb28443d3978d1fd47ffce10000000000000000000000000000006425c019bcda40056233b00000068ff97000000000000000000000000000000052ef09129c4ea4b786856ffbc6fb7526cc73eda753299d7d483339d80809a1d7d89a085d30fc8a7106a170ac2377c34ab973eda753299d7d483339d80809a1d7f8ce65bb936f2d836012914aca65f077e1000000000000000000000000000000681e5d7c70141ebdfe86c0a873114c3b84000000000000000000000000000000297784894e27525bc342b7fde37dba93660000000000000000000000000000000275ecae82e897af7658d0e5be57000640000000000000000000000000000000013af65741744bd7bb2c6872df2b80032000000000000000000000000000000059736a8da406e7d5f0bd1ea7b710796a900000000000000000000000000000000c8557e86f90d0d89eed6eb5349a0f88200453ae02a5f228d8f956b5eab4fc92bbeea5eb26b6ae4b42b4fdfcfdf026aa22000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000020000000000000000073eda753299d7d483339d80809a1d7f454b67d3d21d64bde527fe92f9096edee73eda753299d7d483339d80809a1d7d8efb71c7206e733dbb8e789998e74fb3973eda753299d7d483339d80809a1d7de2fa1eecf722fd187b66bd77b6b8c40c773eda753299d7d483339d80809a1d7d9d7737d61384fec3a4b662fb0b5b9c3b673eda753299d7d483339d80809a1d7de07f99433ad9272abcc573dd286b9ad6973eda753299d7d483339d80809a1d7daf4d6c8b96cfbe51c6c62e3bb537d08bd00000000000000000000000000000021fe0e4d8bbc5020415b002d9eded2242600000000000000000000000000000058c80d0f21f22e50468e30eccae31609900000000000000000000000000000004e48376a671b9d14ee9328510728e77e7400000000000000000000000000000056f8944d438f5cdf896933a09c948c78960000000000000000000000000000004e97881f9ea4d7d2a667518458f28ca53073eda753299d7d4833351088b4af7508df8b737010b26e15294bfcbb9194fffd00000000000000000000020000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000639f3557494a69e4d764d44424b56a655d3cdfc3f4d1e529046aa128fb955ed753f8952b5f3529e3e600fac084e429b93398ae2c32e39086451c73ae91078e9572018b4e10851f49ad26aac06d2b078ce59fa085f4f891b79b75e3a1d6b6d36655d6172d5f790cc00804f1cec8d51a8a7ab4980eeb2a209591f6c4a4787dad723154e7648f18b458a4b667e793d6bd469e46b2bc9c9b191b2461594e5b520d643769f6da3ff19020a635ad3b7a6605e731368d12b414f106fda2579e1d2e445831753f2d1a55002942bafd5a16227a46e361d2d17d6d69a78518a94ef63db0f070eb98e8f3b7e79c61119f491ec5923588b85e2aa35db0d2a62bb3dec0537b5a03d8380a3230bbfd0c265a8f38eda0f0dc3c06fa160b948ec91438ba529259363c2f204b9448e1105669cc7281997af5b21217e829a876d2dc1276b50f04a51e1143d88a0b6c1496e9cd0838e1f45d7817303e89c6c829c8b73d4d62495be5390519b99ea9ba5d06e6ce7d9114d5cc36f15089dd97d479f104bb50c2c5a37751110328f8f4f37cf5adc3dd53dd5ce3778cf9fe60052388aff5cead6113849e21057797fa7060856f215655ff11006fee9a1697597c277945ddaadfac83aad2c00000000000000000000000000000003212e00cde6d2002b119d800000347fcb8000000000000000000000000000000340f2ebe380a0f5eff4360543988a61dc20000000000000000000000000000002cb9b546d20373eaf85e8f53db883cb5480453ae02a5f228d8f956b6eab50092aaff9ec460d8863b22077de62a80bd881273eda753299d7d4833351088b4af7508df8b737010b26601ef73fcbb87bd00000aef2ff4e0c10ade42aca9fe2200e00159ed486fd28ef7edef05bf590de59ef300000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000006000000000000000000000000000073eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffffff73eda753299d7d483339d80809a1d80553bda402fffe5bfefdffffff0000000173eda753299d7d483339d80809a1d80553bba402fffe5bfeffffffff00000001000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000012c71404d368ec010269b10000013afec50000000000000000000000000000000f8cd1b37d4ebee2693904ff354f25f7464000000000000000000000000000001385b1875503c5c39fb9441f95933e4b28c0000000000000000000000000000007c668d9bea75f71349c827f9aa792fba320000000000000000000000000000000761c60b88b9c70e630a72b13b050012c073eda753299d7d483339d80809a1d7a12dfd8a4625be569ccc4ffffef970069173eda753299d7d483339d80809a1d7b264b49166b159a4787a900438048ad93500000000000000000000000000000003b0e305c45ce387318539589d828009600000000000000000000000000000010c5a3fa8ec14b781d2375bf725316c3fb0000000000000000000000000000000259007b94eb27289dcc84c1f9dce2e986073eda753299d7d483339d80809a1d79d35602792ebdf9e00793f578beeb3c47d73eda753299d7d483339d80809a1d802ddd0f5801766ac88a72f1a40a8fff9c173eda753299d7d483339d80809a1d7abe053165ef916860e42e15847ef86957173eda753299d7d483339d80809a1d7ec490dd323de5caac125229595cbe0efc108a75c054be451b1f2ad6bd569f92577dd4bd64d6d5c968569fbf9fbe04d544d000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000060000000000000000073eda753299d7d483339d80809a1d80553bda402fffe5bfefffffff70000000173eda753299d7d483339d80809a1d80553bda402f7fe5bfeffffffff0000000173eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffe00173eda753299d7d483339d80809a1d80553bda402fffe5bdeffffffff0000000173eda753299d7d483339d80809a1d7e355af567743ae3bbda4ffd260212ddbdb73eda753299d7d483339d80809a1d7ac8bb094e10dd00bb871cf13341ce9f67173eda753299d7d483339d80809a1d7b70b86399be46147106cd7aef7d718818d73eda753299d7d483339d80809a1d7ae5b2956bf70a17c7596cc5f626b73876b73eda753299d7d483339d80809a1d7b6bc3584645b26895898ae7ba60d735ad173eda753299d7d483339d80809a1d7b095efed6fd9f96e39d8c5c777a6fa117900000000000000000000000000000065fa2ae8a334f060c4110088dc9c766c7200000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000010a58272d65d68af0d3aa92c660a9421cb00000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000ead8a63f3552d73ecbb978f3157ab67b5c000000000000000000000000000000852c1396b2eb457869d549633054a10e5800000000000000000000000000000104e9bce7caae169e9c3b9ae1d5bda569c20000000000000000000000000000008274de73e5570b4f4e1dcd70eaded2b4e1000000000000000000000000000000ebc6985edbee8777f335f48d0ad7a5ef900000000000000000000000000000007f1cb491dcb90764a7bad754cb0588e5cc73eda753299d7d48333049095fbd120c6b5942dd2166802b5297f978232a0002000000000000000000000600000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000004302515f88a4431e1fbaccbc5adc8f25703b5745de78f77d0d3fe37cf2c01c83140e70dbca64831b4b8f40317b68cd20f34ec27e98adf994cf555b0db316abbd73eda753299d7d483339d60809a1d80553bda402fffe5bfeffffffff0000000173eda751299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001104e71fbe05313635bd503c3e4ec6d9ff680c43f0b2c76d5fb955ed6046aa12a1ff51227ca6853644d38dd4784bdae4c2024f5d6cd1acb78bae38c506ef8716c70156f48f76cc14b27137d78d0b4371477819d08e9f2c77036ebc744ad6da6cb37be870795549c37dcd00b9588085d0fa1ab8c1ad655e52c23ed8949f0fb5ae362a9cec91e3168b1496ccfcf27ad7a8d3c8d65793936323648c2b29cb6a41ac86ed3edb47fe320414c6b5a76f4cc0bce626d1a256829e20dfb44af3c3a5c88b062ea7e5a34aa00528575fab42c44f48dc6c3a5a2fadad34f0a31529dec7b61e06de98a7ebdd251f08ee9668a33e94c65bdb3185246bd05a64c5767be80a6f6b30b88a81e969233f724730fadaac8e2d294b414ee4222bdac5b3caa2ef7b70ba20000000300000000000000000000000000000000000000000000000000000000409fb98f933d25e8d0038d4f7b2a98dbc278a3b57cfb0879943764202d0def5943fe0c177a010031bf648c1cc285529323863340cc562ac9e7aaad86598b55df33cb899e22443dc4bd6718aaa5dd18684590bb9d54587d5a25b7e826dc13afab5a46b0715e6d5198819eb2abc26638708b1b23dc3e7cb23c4a1bb20f9686f7ad0f4d2cdbfd2f1714b46b78b33e8164a4d3f19d98c77d6dd30e31f24850ea65f3419d6a1793664a2e73d2a85da4119e5513d7a0cde3bde4e90718f923a87532fa33097aeadeda76e1094b97fb9816aa66a6edfb200f6a9a0fe16c08233a8dda6309062b3ea1b0c1037678aa3cc094d16f610fd18915e201850d7ce460bf058df50456a29a706afacf2158850711006fe0acf6a437e9477bf6f782dfac86f2cf7b0397cc06bc030aab970dabe70cd498bbeea8daaea65607bb6a872125fec74a1073eda753299d7d4833351088b4af7508df8b737010b26e15294bfcbb919500035e1d3dbecda6214343e24a47f45c5d033197ad01b65a730af95dc57e90c491406bd72f9cfc53af9d931896e77ea5c61244cb6d5fae8954f37dc7b9002f5aa78a4997c5aa3a5fa07bcaf880a9054bef831effbd9cd58e46d9bb4fb88ef99de0db058560108a8005860008060d000b0200011b000021020103070002000085200285400385600485800585a00085c00285e00386000686200786400886600986800a86a00b86c00c86e00d87000e87200487400587600687800787a085200085c00285e00386000487400587600687800787a085400285c00385e00486000587400687600787800f87a085600385c00485e00586000687400787600f87801087a085800485c00585e00686000787400f87601087801187a085a00585c00685e00786000f87401087601187801287a086200685c00785e00f86001087401187601287801387a086400785c00f85e01086001187401287601387801487a01587c01688000b03000121021703070001000085200285400385601885801985a00085c00285e00386001a86201b86400886600986800a86a01c86c01d86e01e87001f87201887401987601a87801b87a085200085c00285e00386001887401987601a87801b87a085400285c00385e01886001987401a87601b87802087a085600385c01885e01986001a87401b87602087802187a085801885c01985e01a86001b87402087602187802287a085a01985c01a85e01b86002087402187602287802387a086201a85c01b85e02086002187402287602387802487a086401b85c02085e02186002287402387602487802587a02687c00b0300011b000121022701000009000686200786400886600986800a86a00b86c00c86e00085200285400385600485800585a00d87000e87201587c01688000b04000121022801000008001a86201b86400886600986800a86a01c86c01d86e00085200285400385601885801985a01e87001f87202687c00b04000121038820290000000b2b0885200985400a85602a85c02b85e02c86000886600986800a86a02d87c02e87e0088520866009852086800a852086a009854086600a854086802f854087200a856086602f8560870030856087202f858086e0308580870031858087202f85a086c03085a086e03185a087003285a087200085c085c02b85c085e02c85c086000385e085e03385e087a0338600878034860087a02f862086a030862086c031862086e0328620870035862087202f8640868030864086a031864086c032864086e0358640870036864087203387408760348740878037874087a03887608760378760878039876087a03a878087803b878087a03c87a087a00d000b050000210388203d03080002150885200985400a85600b85800c85a02a85c02b85e02c86000d86200e86400886600986800a86a00b86c00c86e00d87000e87203e87403f87604087804187a085200886600986800a86a00b86c00c86e00d87000e872085400986600a86800b86a00c86c00d86e00e870042872085600a86600b86800c86a00d86c00e86e042870043872085800b86600c86800d86a00e86c04286e043870044872085a00c86600d86800e86a04286c04386e044870045872085c00085c02b85e02c86003e87403f87604087804187a086200d86600e86804286a04386c04486e045870046872086400e86604286804386a04486c04586e04687004787201587c01688000385e085e03e85e086003f85e087404085e087604185e087804885e087a0058600860040860087404186008760488600878049860087a00787408740488740876049874087804a874087a010876087604a876087804b876087a012878087804c878087a01487a087a00d000b050001210388204d03080001150885200985400a85601c85801d85a02a85c02b85e02c86001e86201f86400886600986800a86a01c86c01d86e01e87001f87204e87404f87605087805187a085200886600986800a86a01c86c01d86e01e87001f872085400986600a86801c86a01d86c01e86e01f870052872085600a86601c86801d86a01e86c01f86e052870053872085801c86601d86801e86a01f86c05286e053870054872085a01d86601e86801f86a05286c05386e054870055872085c00085c02b85e02c86004e87404f87605087805187a086201e86601f86805286a05386c05486e055870056872086401f86605286805386a05486c05586e05687005787202687c00385e085e04e85e086004f85e087405085e087605185e087805885e087a0198600860050860087405186008760588600878059860087a01b87408740588740876059874087805a874087a021876087605a876087805b876087a023878087805c878087a02587a087a00d000b050001210388205d0000000c390885200985400a85602d87c02e87e00088200088400288600388800889200989400a89600085c088400285c088600385c088800885c089200985c089400a85c089600285e088400385e088605e85e089000985e089200a85e089402f85e089e003860088405e860088e038860089000a860089202f860089c030860089e0008660882002868088200386a088205e874088c038874088e05f874089002f874089a030874089c031874089e05e876088a038876088c05f876088e03a876089002f8760898030876089a031876089c032876089e05e8780888038878088a05f878088c03a878088e060878089002f87808960308780898031878089a032878089c035878089e05e87a088603887a088805f87a088a03a87a088c06087a088e03c87a089002f87a089403087a089603187a089803287a089a03587a089c03687a089e00d000b0600002103882061020f000a001688000088200088400288600388800488a00588c00688e00789000889200989400a89600b89800c89a088200086600286800386a00486c00586e006870007872088400085c00285e00386000487400587600687800787a088600285c00385e00486000587400687600787800f87a088800385c00485e00586000687400787600f87801087a088a00485c00585e00686000787400f87601087801187a088c00585c00685e00786000f87401087601187801287a088e00685c00785e00f86001087401187601287801387a089000785c00f85e01086001187401287601387801487a085c00889200989400a89600b89800c89a00d89c00e89e085e00989200a89400b89600c89800d89a00e89c04289e086000a89200b89400c89600d89800e89a04289c04389e087400b89200c89400d89600e89804289a04389c04489e087600c89200d89400e89604289804389a04489c04589e087800d89200e89404289604389804489a04589c04689e087a00e89204289404389604489804589a04689c04789e00885200985400a85600b85800c85a00d86200e86401587c00d89c00e89e00d000b0600012103882062020f0009000088200088400288600388801888a01988c01a88e01b89000889200989400a89601c89801d89a01e89c088200086600286800386a01886c01986e01a87001b872088400085c00285e00386001887401987601a87801b87a088600285c00385e01886001987401a87601b87802087a088800385c01885e01986001a87401b87602087802187a088a01885c01985e01a86001b87402087602187802287a088c01985c01a85e01b86002087402187602287802387a088e01a85c01b85e02086002187402287602387802487a089001b85c02085e02186002287402387602487802587a085c00889200989400a89601c89801d89a01e89c01f89e085e00989200a89401c89601d89801e89a01f89c05289e086000a89201c89401d89601e89801f89a05289c05389e087401c89201d89401e89601f89805289a05389c05489e087601d89201e89401f89605289805389a05489c05589e087801e89201f89405289605389805489a05589c05689e087a01f89205289405389605489805589a05689c05789e00885200985400a85601c85801d85a01e86201f86402687c01f89e00d000b06000121038820630000000b2b6485206585406685606785c06885e06986006786606886806986a02d87c02e87e06a85208520658520854066852085606b854085406c854086406c856086206d856086406c858085a06d858086206e858086406f85a085a06e85a086207085a086406785c086606885c086806985c086a06885e086606985e086807185e0872069860086607186008700728600872073862086207486208640758640864071868087a07186a087807286a087a07186c087607286c087807686c087a07186e087407286e087607686e087807786e087a072870087407687008760778700878078870087a076872087407787208760788720878079872087a00d000b070000210388207a03080002156485206585406685607b85807c85a06785c06885e06986007d86207e86406786606886806986a07f86c08086e08187008287207f87408087608187808287a085206a85206585406685607b85807c85a07d86207e864085c06786606886806986a07f86c08086e081870082872085e06886606986807f86a08086c08186e082870083872086006986607f86808086a08186c08286e083870084872087407f86608086808186a08286c08386e084870085872087608086608186808286a08386c08486e085870086872087808186608286808386a08486c08586e086870087872087a08286608386808486a08586c08686e08787008887201587c01688006b854085407b854085607c854085807d854085a07e8540862089854086408a856085607d856085807e856085a089856086208b856086408c8580858089858085a08b858086208d858086408e85a085a08d85a086208f85a086409086208620918620864092864086400d000b070001210388209303080001156485206585406685609485809585a06785c06885e06986009686209786406786606886806986a09886c09986e09a87009b87209887409987609a87809b87a085206a85206585406685609485809585a096862097864085c06786606886806986a09886c09986e09a87009b872085e06886606986809886a09986c09a86e09b87009c872086006986609886809986a09a86c09b86e09c87009d872087409886609986809a86a09b86c09c86e09d87009e872087609986609a86809b86a09c86c09d86e09e87009f872087809a86609b86809c86a09d86c09e86e09f8700a0872087a09b86609c86809d86a09e86c09f86e0a08700a187202687c06b854085409485408560958540858096854085a09785408620a285408640a385608560968560858097856085a0a285608620a485608640a585808580a2858085a0a485808620a685808640a785a085a0a685a08620a885a08640a986208620aa86208640ab864086400d000b07000121038820ac0000000e100085200285400385606785c06885e06986000086600286800386a02d87c02e87e00088400288600388800885c085c06885c085e06985c086000a85e085e07185e087a0718600878072860087a07187408760728740878076874087a03087608760768760878077876087a0328780878078878087a03687a087a00d000b08000021038820ad04010002150085200285400385600485800585a06785c06885e06986000686200786400086600286800386a00486c00586e00687000787207f87408087608187808287a00088400288600388800488a00588c00688e007890085c00885c06885e06986007f87408087608187808287a01587c01688000a85e085e07f85e086008085e087408185e087608285e087808385e087a00c8600860081860087408286008760838600878084860087a00e874087408387408760848740878085874087a04387608760858760878086876087a0458780878087878087a04787a087a00d000b08000121038820ae04010001150085200285400385601885801985a06785c06885e06986001a86201b86400086600286800386a01886c01986e01a87001b87209887409987609a87809b87a00088400288600388801888a01988c01a88e01b890085c00885c06885e06986009887409987609a87809b87a02687c00a85e085e09885e086009985e087409a85e087609b85e087809c85e087a01d860086009a860087409b860087609c860087809d860087a01f874087409c874087609d874087809e874087a053876087609e876087809f876087a05587808780a0878087a05787a087a00d000b08000105852011852011852005858008060d000b0900000585401185401185400585a008060d000b09000105856011856011856005862008060d000b0900011b00021b000305860008108b200585201185201185800daf060585401185401185a00db0060585601185601186200db1060d000b090001191f00000000000000000000000000000000000000000000000000000000000000000016e98a681dca730dcbe651aec549397611944be61932e7d19a63786871335939b0eefa7e32507b68b4ad6fd89a5c10280000000000000000000000000000000010e6ae8d3becb251e60db7d788be70298dbcdfdbb394da2ca9725e07485d6b630569a66ec867aa0d605573ae82d550df000000000000000000000000000000000fb2ef3076aa1a8068bec7ef35167c1cf1f26a24abaa3b4dc8c6d218021c08429e4c175654b5996eb4de74d17a5c188d00000000000000000000000000000000000f94fa3dcc144ffe6a4ace1de5f95676ad02ffc32c0345767b466f064aec3f1101ecd9eaf91ba5d4c145f792d1285a0000000000000000000000000000000016909a26057dec4152b5bd207c37f9a8c3af2c8cfa63dfd83e34704255392445fae3c47d749e435e0291232659a8caeb0000000000000000000000000000000000b9edb176686ec391a3684928dc084280c60eca091b8a3925e31ef1032793f2ef4e31225ab560bf721f17525dd476c80000000000000000000000000000000014861e20b064f637d60cc250ff3b88049b9697b5245d031c784398bdf950e99d7968c86e92c4ec1cb3821a69686dfb09000000000000000000000000000000000c117b524f84a06b024a40003e1c3aab25253a3cf5b53d2f77d1f9328a9c0b32bea4f591eb470dbb599b5fbdfa7df46b000000000000000000000000000000001420bb2d8c182dda6c982eb820e035f6a4a5c6c69376dfda17ac5588d34a0b313800b8d18ecd818f3e4e2671589ec6910000000000000000000000000000000000855eb6333071d5bb90a0351dbf958c13e5648bcafb55776c78e46031e4620cfda02728e26bb97f067a9a0cdf72aa9f000000000000000000000000000000000707698990b767903c9aaf028c98b46ac4925e4e30db42105aade1a5cd7dd9b3d2222bd7085071b5c2212709cfcc2d46000000000000000000000000000000000a88a7d4f1148bd3efffaedb5be6277727fad92f1005d28d12ef861b5dcf8b580a1322048d469da5f84b26fa7f8d2e4d000000000000000000000000000000001495975f8f8c0cb6c54e1163fe0457322be3eb4af919cb0eb6126028ebeab81d075dff85aa522989166bf6e01880c1be0000000000000000000000000000000018bdd4eef361f4a6d23a6511b48e20fb4080898637660f3f3371adfb48585dd2ad89c29cc260d604ce9cd68a31ff427d00000000000000000000000000000000166c1cd47b07ac0aee553f0b4277cf37b4822422a23ea90486c60a5b3b9052dff113041d51718304e781a5a7bd93bdc200000000000000000000000000000000016c91bfb3ce38ba23df779d69abeaa48aaa142a59667ce4eabfaf36c5453af4aaec6e596ad923693a2a7d1483c2fd20000000000000000000000000000000001262dde7fc4aeca256572897f5d601ef5b8b5586b8593d43cbcd11d2fd80fe2a2214269e5479b5d976cf507d5262c2730000000000000000000000000000000012bd810c9b8eaaf899f161e2d8c26762c3f299919bfa2b2809a027cc9bcac1799ef2d48b11403ce47a5341f7307d2cd400000000000000000000000000000000096d06362268ed80162468c5e664e2bb1a1a3b60a6c419cb6edb470b646e9ed4e021686e93f82d0c3ee2448368fe81be0000000000000000000000000000000015ceb98a36a3576ad6db1163a0a98d2a0535a28ca330834bb07c6682d32b20cd8171f4c0d9f063d768b6e3185b6ae44e0000000000000000000000000000000019731a2f803950b978082f57fdfffb380de3c6bef55492c30e409d77ac9acb523958dea2856b12cb4e87608584ab499a0000000000000000000000000000000005f057d8c91984630d36a0df6c2ebb073881713eb294d8cdf7bb0374cb722b7bb5080101a54638216ab4886f0276a4750000000000000000000000000000000004d63c6ffcf22c5a45cbc67e299c2b3dcfd98199c525785d127006aca0f82774876a94e120b9bde61b9227ee96dc68410000000000000000000000000000000002385e4a7b9ab571a73f07d9574152e5a45de038325c587523413d98b5feb6d2071c293abbeab53bf941d76b264f3369000000000000000000000000000000000eea9ee53f693242e50678f2ac4a50536bc4aa60deae574c46e3cf56a8d14af17d603f8c222728fb09300eee5c5b326b0000000000000000000000000000000014aa3a043bb1340472428a1756ad82bb49fa2fee5ff945799006e851969947efb6351c7642b2683deb31537ea6209643000000000000000000000000000000000b7028c202ee5b6a74d6ccfe5990dc984381d45aa22ccb019d848829dd72d16143e5af158899fcc009d5ca17c97c34140000000000000000000000000000000006c6ca7119058dbb54d5302f125f9cefdc6b68957c7b970427a4b5a3bd0aa7ff5fa033687afdc2a4e150a408b0a5f4d20000000000000000000000000000000008a540ee790774da565637ec30ced988563c90d314d01996465fa3f893d5fb010b617a4edcabc5c8f8141584792a006700000000000000000000000000000000071e81991f7c8f62be7df37cd485cb5e1f941d942cc15f814350867bcdf1521ef6e7e76b6928827688b0d3d14a6cf1a30000000000000000000000000000000005a4ed142d6c05e535a91ff8244ca5cbdda66355e156c19ea759a1f5aecd1225e16cb3f18397a66b055a2918a2a74d54000000000000000000000000000000000ce5eb9724134cce6feb1c0ff1b66ab35e79b2b4087200f1d9a76ee4a2b3b5424266da9d518653e9391e9a94196f1d7f0000000000000000000000000000000009b07c1df13d348c1ed7cb560fcdd68255221aadb75410fa4526857c95cea6bf3a0efa98e31898de56ea7ff30f7c514a0000000000000000000000000000000011b2d20f8b12527aba78064c809753e221cebde9bb41aa67abae156e3a179655388e51211ba5614fb696a7f0cc2125600000000000000000000000000000000010ce280498c90ca7501427caf6bcd6d9b4987380f8d9e8ef710b81034a460c8da32362568e1e9fd6b2cb5297805c98ad000000000000000000000000000000000292967ae2e91be4d3555cddb84538dd0edcb077ce0626f751bee2673ff6b24c182fd2ae99a0cd83881a81a3facfcb6e0000000000000000000000000000000017cf661d855e771f61cab52918bc03e5329761bebc6704041c0430c64bc2d589210a4250f57e9dec51a807d4d6926f31000000000000000000000000000000000f50cacb58bf9101f3d7926b2e26675d036c7cf34f335946dfc1d1a6e148de3da13cabfe8e7507f10baca11e072314130000000000000000000000000000000016caf868eda0aeb753fd8ec3bee96cbe3217eade0bae8e908b6cfe1f144518cbaec5bbbbc6e6641d82702b3dd5997985000000000000000000000000000000000a35da0b540c40574d639e726b9c7e6db45e6cbefb614f14a1721bdbca2dfcb20aaeb64926032522ba3de23420dc1d87000000000000000000000000000000000b7f1691be78a38c579c2ac6bef18d54f2d63e634590931fda91cce7af80f7c589cc37a98870e127c93e059ff020df070000000000000000000000000000000012582699af0164d4335252425d0251c4a1af4e259a3f65f0065b2b05127538a8434de1326c4920ee45da346bbbc7d293000000000000000000000000000000000e004abc0ac243147c480dca32186a3478642aaafe922adde54e02ae93479fa0ae45b75ed0229c2f358a0473d17c09a50000000000000000000000000000000013e0dac8358fff676678a7b7d854aee33fda118cfb39d426f3418966ed06283adbd804fbe8cf51fd89c2f5c410919f4800000000000000000000000000000000168c0d6883d0d5b67d3a5944ee0616a3ce7ac537685a26b678bab6b1b50115c32a5f90b6ca56cb8560a40964840893ce00000000000000000000000000000000197d467f656fc49a1c5119d7cd826cc2be6345c67f108e7e44f8ebfcebea5366fdee85d69947c8f74f623165dc1029b500000000000000000000000000000000133f78d1d5ad537d358f421d115bdb2f44eff93e623017fb4a06006877bd8504b8b614c6c877d73de7cdc2c2e69a899800000000000000000000000000000000067b406b38e3895daddca2f64de3ee215c7961f1d2f1904f7e2aa60bc20559610c868810183505527e28eea6089d6bf8000000000000000000000000000000000bd679e559d6bf9f498928a3074a070923c4cf1d5823b59316852fd6653fe003a4a2da9ba443e4640993524c79a6f56d000000000000000000000000000000000821077b092335b9f70fb79a424cca134e823d96a4fdeaf4154ab7c5ef023844695a10e9bfdf314847ccaa40205f980a00000000000000000000000000000000041f7c88095a1bbf5759d88a7d1098534b1661f3b1b9012866e17c7550a5b52861ac915beba38767c7aeb04c1df330ac000000000000000000000000000000000f2e8cc95c27bf0235efa88fa70bb3eb59a5ef2602a9b70c5af7b0db755f10a8e0ffc380b61a580c3300fda8d6077854000000000000000000000000000000001983933b9a69c960c9eda1f5942af11a22de694e7c8b2a5f8d920b50d6c90aca30d149ace5ff9feebf84a136abdfa9f400000000000000000000000000000000162f68d07437f5dd432392f6a244044d4605b4457384fb65577df046c667a744f851fbdb0e8751aaeeb334bf32fbd3e00000000000000000000000000000000014669fe860d72f984d085b33bfffa39912a9b0d0acb1ff8fd119eee9d0870eab9324293b88df67b9e1ebd180db262c72000000000000000000000000000000000f3292f0f7b5707fe0874c56fdae2e882a2ba44cc1a14f736d255905838f8438fb8dbfd65ad84bcbca748a749c58955e000000000000000000000000000000000ee7e9d684b203f28dd5424f3a511695ff88a9d696976adca8c54262e0f553756ec45b8e103453f8c18a07c3abcef5fd00000000000000000000000000000000177029152e262c258c987bfbeeee14c8e945c114efada4abdeff8610bea0cf199ef3f375cf15f6b437de67f3379c5801000000000000000000000000000000000e2ead1ec1acc411940e7bea5d0f21146b39327e36ebab84013dd8f5c2b3f8b576a125db88ff28db45a79a28df414b6600000000000000000000000000000000199b4ea033c01e1d83627c054ba6f2dc2a61b1277d07469ad65c413a1970c9cf179bedfc4cf2f61c1ee8ca5e4a01ab900000000000000000000000000000000011af8f4e99c419588e05414f664ae8f6dded731799d69d8f5a72c124a5c6df33153e15767000ef54648a8dd23687140a0000000000000000000000000000000017ba99d9323e45fe54ec6ac4daaad8d30c0dbc53d3f5f54c05667eb0e4f28da8973657bf62915fcdfc551698f56aa574000000000000000000000000000000000ed68fa806cd3171cb061afebf65799df9014492a08de2420f1234158d2f1d214b5354b7ad8cfc753d2712e6bb9b20a50000000000000000000000000000000013f05c6e0393baeec02345108200735355211ac5dd3c2267dcf706d18263fa7b7916e44ab1d5309430542f75717187c4000000000000000000000000000000000a925e4ad4c2d71f096de0e53baea7207a90226e678437d20f2ec9ae630075a12ae88a186bcfba1bc0444e5bc40245e70000000000000000000000000000000006ff1ce2b5e6c2c3cedd7a8465c5ed3a72e4496eead09ba2cd0ef56fdd94942ed738b42b71caea3d1bd78b0ae3ec3f96000000000000000000000000000000000190af4fe845524352a9f624463ae7ca406fa14f4319031fe5a7630ab2bd66c107d6cb05fa65b98ee0c5f0234c29d1890000000000000000000000000000000013036a5f7dca90b530955bf735da292029b8759dc69e53d6c830a3bee081165d4d59442dacb50c2a06aded3193e883870000000000000000000000000000000012b6e22b800c1a2dfde554f3312f4b501b75400de7c8c476dcc9fd72dcef80d9384da4e4af8f928a07e4a9fa485da8bc00000000000000000000000000000000053da19ebda674bbc95d3d10ad04cb96a410aecead2c7059df1e12a1a56117a830f0008a91551cec88f1ad548a379dfe0000000000000000000000000000000016038223f354e7d7c75614116c58b1c9866a8a39d6b30ea560b263c68da7e78210b2f2a7f9b19564388f2f2370d7cd6a000000000000000000000000000000000bdb1d40f473070bfa087ff87bc9c86d0d57398e71f45845ebcd0aa24a9ea829718dfce700af80fab46b51ad73fb58f600000000000000000000000000000000085c579504429306bbf0ec28b07f91a67326fabf9ec866a45ef4d294f390a7c497c7b990a62b602f5999fcaee0e7e9bc000000000000000000000000000000000b042dab28398e8c0670ee6f04cca93aa41ecc7205c8216513d027b4c1213d0e452c16898d7b2c811027598cf2715dc300000000000000000000000000000000093b6ec41dd5c2455b9d2dca635c472d386496c219c45db9f1a5d96c179d1028694626c4a1845aef384efaaa8053284b000000000000000000000000000000000386d1128886a4a879e8a93aa84b65251061026f4dae08952816961f9d6fc0b8153784a5961f831b68f1e2ddc567efb70000000000000000000000000000000000b6ed6814a099c4e435e42fc8ee42500e16b88c78069cebe20e4d647c8bb44acd6b9c6712d53a3ebd014e54a1a75e890000000000000000000000000000000001ae3805b59b2f2d53a59e2c19001e97aace57ac2ba8b58550a740e897ba6278e409291e5c87466a01edd7cbe3b99f88000000000000000000000000000000000ca3af5a65ec51141f694db29736dd57a1cdbfa79f45aa80a6cdda8de3d51c4b16c95361b412c6f47e132eae7b69f7f70000000000000000000000000000000018dc16284e57fccd755058b259b913c77a54da0d0e206b7906878fd3e994ad04c66f48e19cd982d5651f99b48b99206400000000000000000000000000000000197560c82bf47486bee750469d626ce42d40ce7489ff61a64a0eb55e6672926a40f06348ab575f926fbe4d87b672fcd70000000000000000000000000000000007b4f39a579a01d4f01f4ab9bc841a7de70541d6b79945e9569fd59388739f5b4ce57794044d287d7b0b58e35198caab0000000000000000000000000000000004e1b539a3a123291eaa8c85be3395e79c6845178d995313daf42680692210e1ec79278ec8b6e2da1d6f2896091004040000000000000000000000000000000010d56b62afe35890b4f98b946daa8936d83e014595585e2892e21bfc5e86f84d404b435dc1a689539d74773aba55dad4000000000000000000000000000000000f227bcaef796cabf0a93289fb8321e91d43880fa5fcebab73c77c6bb406cc5e9386a81774635b5a6144b75ef10cd994000000000000000000000000000000000a849d95d37e501cc6272f325f88d677b25be83ce57eb9c32d382613930bbc0ef7ec349d44ac74a9383c111efcb73783000000000000000000000000000000000b77ada25b279742ec75d2014583ad79427bfa99ab8a941648e1a11084fec2a6fdb8192f498cf3e272a5e786597273a100000000000000000000000000000000056788c108fd40583888b8771a70d3f6364290f978d1fc201c0a76561a4c1cdde7643f7d76448dad2586c3eac2e341040000000000000000000000000000000002ecf71916494d46153fdd513ead191706401b48839713727c920f4591fe09ac3e8f4140ef1cd9e39b0d0bbe0b5b87d4000000000000000000000000000000000e7bab8cc3c714f06826c8f3695dc63ce7e931ff0dbe009cbb84fef0b99124905d90cdd6f5356a7ee7673548f6f0692c diff --git a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2VerifyingKey.sol b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2VerifyingKey.sol new file mode 100644 index 000000000..32ff11e39 --- /dev/null +++ b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2VerifyingKey.sol @@ -0,0 +1,693 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.24; + +/// @title Halo2 BLS12-381 verifying-key payload. +/// @notice Contract whose deployed runtime is `INVALID || generated verifier-key payload`. +/// @dev Byte 0 is an unconditional INVALID opcode so direct calls cannot execute payload bytes as code. The linked verifier pins the full runtime by length/codehash and copies the payload starting at byte 1. +/// @dev The layout follows the verifier inputs derived from +/// `midfall/proofs/src/plonk/mod.rs::VerifyingKey` and the transcript +/// `vk.hash_into` behavior used by `midfall/proofs/src/plonk/verifier.rs`. +/// +/// Layout (in 32-byte words, big-endian). The header slots are generated from +/// Rust's `VkHeaderLayout`; the byte offsets are absolute from the start of the +/// VK payload, not from byte 0 of the runtime. Runtime byte 0 is the INVALID +/// prefix; the verifier loads the payload via +/// `extcodecopy(vk, VK_MPTR, 0x01, vk_payload_len)` and then references each +/// slot by `VK_MPTR + i`. +/// +/// word 0 : vk_digest (Fq, transcript_repr of the CS) +/// word 1 : num_instances +/// word 2 : k (log2 of the domain size) +/// word 3 : n_inv (1/n in Fr) +/// word 4 : omega (n-th primitive root of unity) +/// word 5 : omega_inv +/// word 6 : omega_inv_to_l (omega_inv ^ |rotation_last|) +/// word 7 : has_accumulator (0 or 1) +/// word 8 : acc_offset (instance index of the accumulator) +/// word 9 : num_acc_limbs +/// word 10 : num_acc_limb_bits +/// word 11..14 : G1_BASE (4 words, EIP-2537 padded) +/// word 15..22 : G2_BASE (8 words, EIP-2537 padded) +/// word 23..30 : NEG_S_G2_BASE (8 words, EIP-2537 padded) +/// word 31..30 + Q_PAYLOAD : quotient VM constants + packed bytecode +/// word 31 + Q_PAYLOAD .. : fixed_comms (4 words each) +/// word 31 + Q_PAYLOAD + 4*N_FIXED .. +/// : permutation_comms (4 words each) +/// +/// Notes: +/// - `extcodehash` of this contract is pinned by the linked verifier via +/// `EXPECTED_VK_CODEHASH`, so any byte tweak is detected at deploy time. +/// - The quotient identity interpreter's static program is stored in this +/// pinned VK runtime. The verifier reads it from memory after `extcodecopy`, +/// avoiding verifier-side PUSH32/mstore immediates while keeping the program +/// covered by `EXPECTED_VK_CODEHASH`. +/// - The midnight-proofs migration bakes the per-lookup chunk counts, trashcan +/// structure, and `num_simple_selectors` into the generated verifier code. +contract Halo2VerifyingKey { + /// @notice Deploy the verifying-key payload as this contract's runtime bytecode. + /// @dev The constructor writes an INVALID byte followed by generated words into memory and returns that prefixed runtime. + /// @dev The transient construction buffer starts at `0x80`, preserving Solidity's reserved memory words. + constructor() { + assembly { + // Runtime layout: + // byte 0 : INVALID, so the payload cannot be executed + // byte 1..end : generated VK payload copied by Halo2Verifier + // + // `runtime` includes the INVALID prefix; `payload` points to word + // zero of the verifier-key data described in the contract NatSpec. + let runtime := 0x80 + let payload := add(runtime, 0x01) + mstore8(runtime, 0xfe) + // Header, base-point, and quotient-program words generated from + // VkPayloadLayout. The inline names on each mstore identify the + // exact slot in the rendered source. + mstore(add(payload, 0x0000), 0x56c0824fcff237dd8dc7b15f527346d9e1647d191815acb142500b0293e84f66) // vk_digest + mstore(add(payload, 0x0020), 0x0000000000000000000000000000000000000000000000000000000000000013) // num_instances + mstore(add(payload, 0x0040), 0x0000000000000000000000000000000000000000000000000000000000000014) // k + mstore(add(payload, 0x0060), 0x73eda0144f284aae5b6554d46c21576b363d4ec725be2bff1a400fff00001001) // n_inv + mstore(add(payload, 0x0080), 0x03e1c54bcb947035a57a6e07cb98de4a2f69e02d265e09d9fece7e0e39898d4b) // omega + mstore(add(payload, 0x00a0), 0x6c39442eade0092768ac033fa6f608750624a1bb17dbc026ef97c3573a28fc8c) // omega_inv + mstore(add(payload, 0x00c0), 0x2a0ccbaa0613f093f2bb6e97859513f0b613d8587eaa92db9e5604b8d6b68d45) // omega_inv_to_l + mstore(add(payload, 0x00e0), 0x0000000000000000000000000000000000000000000000000000000000000001) // has_accumulator + mstore(add(payload, 0x0100), 0x000000000000000000000000000000000000000000000000000000000000000b) // acc_offset + mstore(add(payload, 0x0120), 0x0000000000000000000000000000000000000000000000000000000000000007) // num_acc_limbs + mstore(add(payload, 0x0140), 0x0000000000000000000000000000000000000000000000000000000000000038) // num_acc_limb_bits + mstore(add(payload, 0x0160), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) // g1_x_hi + mstore(add(payload, 0x0180), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) // g1_x_lo + mstore(add(payload, 0x01a0), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) // g1_y_hi + mstore(add(payload, 0x01c0), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) // g1_y_lo + mstore(add(payload, 0x01e0), 0x00000000000000000000000000000000024aa2b2f08f0a91260805272dc51051) // g2_x_c0_hi + mstore(add(payload, 0x0200), 0xc6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8) // g2_x_c0_lo + mstore(add(payload, 0x0220), 0x0000000000000000000000000000000013e02b6052719f607dacd3a088274f65) // g2_x_c1_hi + mstore(add(payload, 0x0240), 0x596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e) // g2_x_c1_lo + mstore(add(payload, 0x0260), 0x000000000000000000000000000000000ce5d527727d6e118cc9cdc6da2e351a) // g2_y_c0_hi + mstore(add(payload, 0x0280), 0xadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801) // g2_y_c0_lo + mstore(add(payload, 0x02a0), 0x000000000000000000000000000000000606c4a02ea734cc32acd2b02bc28b99) // g2_y_c1_hi + mstore(add(payload, 0x02c0), 0xcb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be) // g2_y_c1_lo + mstore(add(payload, 0x02e0), 0x000000000000000000000000000000000632aaf712568f19c297802268a7ad9d) // neg_s_g2_x_c0_hi + mstore(add(payload, 0x0300), 0xceea6ef7ab6f75a7d26781c8e90c7432bc5e99dcc219ba64010f3052123983ab) // neg_s_g2_x_c0_lo + mstore(add(payload, 0x0320), 0x00000000000000000000000000000000191ff4920e077a2f8cb3969ba8f05bc2) // neg_s_g2_x_c1_hi + mstore(add(payload, 0x0340), 0xaa9da8c95d640b1e051be7cf344ee7f01996df2568bf0e7ccd9eb70978820045) // neg_s_g2_x_c1_lo + mstore(add(payload, 0x0360), 0x0000000000000000000000000000000005f434ebf45460a864ad5b17497c7903) // neg_s_g2_y_c0_hi + mstore(add(payload, 0x0380), 0x71820c70c83aa186029536d22dff54373251152c28bc43269f95281eba1b012e) // neg_s_g2_y_c0_lo + mstore(add(payload, 0x03a0), 0x0000000000000000000000000000000004d1c747141bcac15e77e3e1d3853254) // neg_s_g2_y_c1_hi + mstore(add(payload, 0x03c0), 0xc8687afdad35345a04f79d9c2759007f6640676eb44aee7011ce5ad80744bb23) // neg_s_g2_y_c1_lo + mstore(add(payload, 0x03e0), 0x0000000000000000000000000000000000000000000000000000000000000001) // quotient_const + mstore(add(payload, 0x0400), 0x00bbe1fbe9ef1e2d62490b03a82bf9ef10f5e9b2323033669cf6c50481f63e05) // quotient_const + mstore(add(payload, 0x0420), 0x0000000000000000000000000000000000000000000000000100000000000000) // quotient_const + mstore(add(payload, 0x0440), 0x0000000000000000000000000000000000010000000000000000000000000000) // quotient_const + mstore(add(payload, 0x0460), 0x0000000000000000000000000000000000000000000000000000000400000000) // quotient_const + mstore(add(payload, 0x0480), 0x0000000000000000000000000000000000000000040000000000000000000000) // quotient_const + mstore(add(payload, 0x04a0), 0x0000000000000000000000000000000000000000000000000000000000001000) // quotient_const + mstore(add(payload, 0x04c0), 0x0000000000000000000000000000000000000000000000100000000000000000) // quotient_const + mstore(add(payload, 0x04e0), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000) // quotient_const + mstore(add(payload, 0x0500), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefeffffff00000001) // quotient_const + mstore(add(payload, 0x0520), 0x73eda753299d7d483339d80809a1d80553bca402fffe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x0540), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffb00000001) // quotient_const + mstore(add(payload, 0x0560), 0x73eda753299d7d483339d80809a1d80553bda402fbfe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x0580), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffefffff001) // quotient_const + mstore(add(payload, 0x05a0), 0x73eda753299d7d483339d80809a1d80553bda402fffe5beeffffffff00000001) // quotient_const + mstore(add(payload, 0x05c0), 0x00000000000000000000000000000010ff0726c5de281020ad8016cf6f691213) // quotient_const + mstore(add(payload, 0x05e0), 0x0000000000000000000000000000002c64068790f917282347187665718b04c8) // quotient_const + mstore(add(payload, 0x0600), 0x00000000000000000000000000000027241bb5338dce8a77499428839473bf3a) // quotient_const + mstore(add(payload, 0x0620), 0x0000000000000000000000000000002b7c4a26a1c7ae6fc4b499d04e4a463c4b) // quotient_const + mstore(add(payload, 0x0640), 0x000000000000000000000000000000274bc40fcf526be95333a8c22c79465298) // quotient_const + mstore(add(payload, 0x0660), 0x0000000000000000000000000000002a5ee6db49930276e2939d1c43ac82f744) // quotient_const + mstore(add(payload, 0x0680), 0x73eda753299d7d483339d80809a1d7edd77e26c51c38afb5debf8afa00c15cc3) // quotient_const + mstore(add(payload, 0x06a0), 0x73eda753299d7d483339d80809a1d7c553bda402fffe5bfeffffffff00000002) // quotient_const + mstore(add(payload, 0x06c0), 0x73eda753299d7d483339d80809a1d80553bda402fffe53ebc627fffef6280001) // quotient_const + mstore(add(payload, 0x06e0), 0x0000000000000000000001000000000000000000000000000000000000000000) // quotient_const + mstore(add(payload, 0x0700), 0x0000000100000000000000000000000000000000000000000000000000000000) // quotient_const + mstore(add(payload, 0x0720), 0x6bc66e553973f396854f5626172ba135587d41e37a68209402355093fdcaaf6c) // quotient_const + mstore(add(payload, 0x0740), 0x63f31e3f446953960c9d6964474300df43ab29179970f642a28e39d6c883c74b) // quotient_const + mstore(add(payload, 0x0760), 0x73eda753299d7d483339d70809a1d80553bda402fffe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x0780), 0x73eda752299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x07a0), 0x082738fdf02989b1adea81e1f27636cffb40621f85963b6afdcaaf6b02355095) // quotient_const + mstore(add(payload, 0x07c0), 0x0ffa8913e53429b2269c6ea3c25ed72610127aeb668d65bc5d71c628377c38b6) // quotient_const + mstore(add(payload, 0x07e0), 0x01ec1c0519185dfe86132d479c76d0786e1e037d0b05ca47648a1c5d29492c9b) // quotient_const + mstore(add(payload, 0x0800), 0x1e179025ca2470882b34e63940ccbd7ad9090bf414d43b696e093b5a8782528f) // quotient_const + mstore(add(payload, 0x0820), 0x4298bfee9a84c8ef8e83702075cb1abeb576f146636342e3db9ea6b0a4adf29d) // quotient_const + mstore(add(payload, 0x0840), 0x3c83b078e9abed278d042acc8f3bd21e228716f04be96af8025da860e2d1bba9) // quotient_const + mstore(add(payload, 0x0860), 0x427868260f487d1ef07edaadf37f5dbe705bd1318290f2577ae756b009c24f11) // quotient_const + mstore(add(payload, 0x0880), 0x03020e6a35e595abd22838beeadc45cfcb0545d85ca0ab2c59d44c203fac84a7) // quotient_const + mstore(add(payload, 0x08a0), 0x000000000000000000000000000000000000000000000000d201000000010000) // quotient_const + mstore(add(payload, 0x08c0), 0x0000000100001b7c3f8d3fe3c5b448f1bdeb2ae34698b72d6ce966fc208c05ed) // quotient_const + mstore(add(payload, 0x08e0), 0x73eda753299d7d483339d80809a1d7fd4057a4c12f26d1c1778e3360a6820001) // quotient_const + mstore(add(payload, 0x0900), 0x057797fa7060856f215654ff11006fe0acf6a437e9477bf6f782dfac86f2cf75) // quotient_const + mstore(add(payload, 0x0920), 0x0000000000000000000000000000000000000000000000000000000000000002) // quotient_const + mstore(add(payload, 0x0940), 0x0000000000000000000000000000000000000000000000000200000000000000) // quotient_const + mstore(add(payload, 0x0960), 0x0000000000000000000000000000000000020000000000000000000000000000) // quotient_const + mstore(add(payload, 0x0980), 0x73eda753299d7d483339d80809a1d7e13511a4044eaa5bff4600ffff00005556) // quotient_const + mstore(add(payload, 0x09a0), 0x73eda753299d7d483339d80809a1d7c553bda402fffe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x09c0), 0x73eda753299d7d483339d80809a1d7d340dd972492de594de627fffefcb80349) // quotient_const + mstore(add(payload, 0x09e0), 0x73eda753299d7d483339d80809a1d7dbdc391ab4d8ac003bbd48021b82456c9b) // quotient_const + mstore(add(payload, 0x0a00), 0x73eda753299d7d483339d80809a1d7d1448ee5caf5eefcffbc9fabc57759e23f) // quotient_const + mstore(add(payload, 0x0a20), 0x73eda753299d7d483339d80809a1d80418c74cc18bb28443d3978d1fd47ffce1) // quotient_const + mstore(add(payload, 0x0a40), 0x0000000000000000000000000000006425c019bcda40056233b00000068ff970) // quotient_const + mstore(add(payload, 0x0a60), 0x00000000000000000000000000000052ef09129c4ea4b786856ffbc6fb7526cc) // quotient_const + mstore(add(payload, 0x0a80), 0x73eda753299d7d483339d80809a1d7d89a085d30fc8a7106a170ac2377c34ab9) // quotient_const + mstore(add(payload, 0x0aa0), 0x73eda753299d7d483339d80809a1d7f8ce65bb936f2d836012914aca65f077e1) // quotient_const + mstore(add(payload, 0x0ac0), 0x000000000000000000000000000000681e5d7c70141ebdfe86c0a873114c3b84) // quotient_const + mstore(add(payload, 0x0ae0), 0x000000000000000000000000000000297784894e27525bc342b7fde37dba9366) // quotient_const + mstore(add(payload, 0x0b00), 0x0000000000000000000000000000000275ecae82e897af7658d0e5be57000640) // quotient_const + mstore(add(payload, 0x0b20), 0x000000000000000000000000000000013af65741744bd7bb2c6872df2b800320) // quotient_const + mstore(add(payload, 0x0b40), 0x00000000000000000000000000000059736a8da406e7d5f0bd1ea7b710796a90) // quotient_const + mstore(add(payload, 0x0b60), 0x0000000000000000000000000000000c8557e86f90d0d89eed6eb5349a0f8820) // quotient_const + mstore(add(payload, 0x0b80), 0x0453ae02a5f228d8f956b5eab4fc92bbeea5eb26b6ae4b42b4fdfcfdf026aa22) // quotient_const + mstore(add(payload, 0x0ba0), 0x0000000000000000000000000000000000000000000000000000000800000000) // quotient_const + mstore(add(payload, 0x0bc0), 0x0000000000000000000000000000000000000000080000000000000000000000) // quotient_const + mstore(add(payload, 0x0be0), 0x0000000000000000000000000000000000000000000000000000000000002000) // quotient_const + mstore(add(payload, 0x0c00), 0x0000000000000000000000000000000000000000000000200000000000000000) // quotient_const + mstore(add(payload, 0x0c20), 0x73eda753299d7d483339d80809a1d7f454b67d3d21d64bde527fe92f9096edee) // quotient_const + mstore(add(payload, 0x0c40), 0x73eda753299d7d483339d80809a1d7d8efb71c7206e733dbb8e789998e74fb39) // quotient_const + mstore(add(payload, 0x0c60), 0x73eda753299d7d483339d80809a1d7de2fa1eecf722fd187b66bd77b6b8c40c7) // quotient_const + mstore(add(payload, 0x0c80), 0x73eda753299d7d483339d80809a1d7d9d7737d61384fec3a4b662fb0b5b9c3b6) // quotient_const + mstore(add(payload, 0x0ca0), 0x73eda753299d7d483339d80809a1d7de07f99433ad9272abcc573dd286b9ad69) // quotient_const + mstore(add(payload, 0x0cc0), 0x73eda753299d7d483339d80809a1d7daf4d6c8b96cfbe51c6c62e3bb537d08bd) // quotient_const + mstore(add(payload, 0x0ce0), 0x00000000000000000000000000000021fe0e4d8bbc5020415b002d9eded22426) // quotient_const + mstore(add(payload, 0x0d00), 0x00000000000000000000000000000058c80d0f21f22e50468e30eccae3160990) // quotient_const + mstore(add(payload, 0x0d20), 0x0000000000000000000000000000004e48376a671b9d14ee9328510728e77e74) // quotient_const + mstore(add(payload, 0x0d40), 0x00000000000000000000000000000056f8944d438f5cdf896933a09c948c7896) // quotient_const + mstore(add(payload, 0x0d60), 0x0000000000000000000000000000004e97881f9ea4d7d2a667518458f28ca530) // quotient_const + mstore(add(payload, 0x0d80), 0x73eda753299d7d4833351088b4af7508df8b737010b26e15294bfcbb9194fffd) // quotient_const + mstore(add(payload, 0x0da0), 0x0000000000000000000002000000000000000000000000000000000000000000) // quotient_const + mstore(add(payload, 0x0dc0), 0x0000000200000000000000000000000000000000000000000000000000000000) // quotient_const + mstore(add(payload, 0x0de0), 0x639f3557494a69e4d764d44424b56a655d3cdfc3f4d1e529046aa128fb955ed7) // quotient_const + mstore(add(payload, 0x0e00), 0x53f8952b5f3529e3e600fac084e429b93398ae2c32e39086451c73ae91078e95) // quotient_const + mstore(add(payload, 0x0e20), 0x72018b4e10851f49ad26aac06d2b078ce59fa085f4f891b79b75e3a1d6b6d366) // quotient_const + mstore(add(payload, 0x0e40), 0x55d6172d5f790cc00804f1cec8d51a8a7ab4980eeb2a209591f6c4a4787dad72) // quotient_const + mstore(add(payload, 0x0e60), 0x3154e7648f18b458a4b667e793d6bd469e46b2bc9c9b191b2461594e5b520d64) // quotient_const + mstore(add(payload, 0x0e80), 0x3769f6da3ff19020a635ad3b7a6605e731368d12b414f106fda2579e1d2e4458) // quotient_const + mstore(add(payload, 0x0ea0), 0x31753f2d1a55002942bafd5a16227a46e361d2d17d6d69a78518a94ef63db0f0) // quotient_const + mstore(add(payload, 0x0ec0), 0x70eb98e8f3b7e79c61119f491ec5923588b85e2aa35db0d2a62bb3dec0537b5a) // quotient_const + mstore(add(payload, 0x0ee0), 0x03d8380a3230bbfd0c265a8f38eda0f0dc3c06fa160b948ec91438ba52925936) // quotient_const + mstore(add(payload, 0x0f00), 0x3c2f204b9448e1105669cc7281997af5b21217e829a876d2dc1276b50f04a51e) // quotient_const + mstore(add(payload, 0x0f20), 0x1143d88a0b6c1496e9cd0838e1f45d7817303e89c6c829c8b73d4d62495be539) // quotient_const + mstore(add(payload, 0x0f40), 0x0519b99ea9ba5d06e6ce7d9114d5cc36f15089dd97d479f104bb50c2c5a37751) // quotient_const + mstore(add(payload, 0x0f60), 0x110328f8f4f37cf5adc3dd53dd5ce3778cf9fe60052388aff5cead6113849e21) // quotient_const + mstore(add(payload, 0x0f80), 0x057797fa7060856f215655ff11006fee9a1697597c277945ddaadfac83aad2c0) // quotient_const + mstore(add(payload, 0x0fa0), 0x0000000000000000000000000000003212e00cde6d2002b119d800000347fcb8) // quotient_const + mstore(add(payload, 0x0fc0), 0x000000000000000000000000000000340f2ebe380a0f5eff4360543988a61dc2) // quotient_const + mstore(add(payload, 0x0fe0), 0x0000000000000000000000000000002cb9b546d20373eaf85e8f53db883cb548) // quotient_const + mstore(add(payload, 0x1000), 0x0453ae02a5f228d8f956b6eab50092aaff9ec460d8863b22077de62a80bd8812) // quotient_const + mstore(add(payload, 0x1020), 0x73eda753299d7d4833351088b4af7508df8b737010b26601ef73fcbb87bd0000) // quotient_const + mstore(add(payload, 0x1040), 0x0aef2ff4e0c10ade42aca9fe2200e00159ed486fd28ef7edef05bf590de59ef3) // quotient_const + mstore(add(payload, 0x1060), 0x0000000000000000000000000000000000000000000000000000000000000006) // quotient_const + mstore(add(payload, 0x1080), 0x0000000000000000000000000000000000000000000000000600000000000000) // quotient_const + mstore(add(payload, 0x10a0), 0x0000000000000000000000000000000000060000000000000000000000000000) // quotient_const + mstore(add(payload, 0x10c0), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffffff) // quotient_const + mstore(add(payload, 0x10e0), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefdffffff00000001) // quotient_const + mstore(add(payload, 0x1100), 0x73eda753299d7d483339d80809a1d80553bba402fffe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x1120), 0x0000000000000000000000000000000000000000000000000000000000000003) // quotient_const + mstore(add(payload, 0x1140), 0x0000000000000000000000000000000000030000000000000000000000000000) // quotient_const + mstore(add(payload, 0x1160), 0x0000000000000000000000000000012c71404d368ec010269b10000013afec50) // quotient_const + mstore(add(payload, 0x1180), 0x000000000000000000000000000000f8cd1b37d4ebee2693904ff354f25f7464) // quotient_const + mstore(add(payload, 0x11a0), 0x000000000000000000000000000001385b1875503c5c39fb9441f95933e4b28c) // quotient_const + mstore(add(payload, 0x11c0), 0x0000000000000000000000000000007c668d9bea75f71349c827f9aa792fba32) // quotient_const + mstore(add(payload, 0x11e0), 0x0000000000000000000000000000000761c60b88b9c70e630a72b13b050012c0) // quotient_const + mstore(add(payload, 0x1200), 0x73eda753299d7d483339d80809a1d7a12dfd8a4625be569ccc4ffffef9700691) // quotient_const + mstore(add(payload, 0x1220), 0x73eda753299d7d483339d80809a1d7b264b49166b159a4787a900438048ad935) // quotient_const + mstore(add(payload, 0x1240), 0x00000000000000000000000000000003b0e305c45ce387318539589d82800960) // quotient_const + mstore(add(payload, 0x1260), 0x0000000000000000000000000000010c5a3fa8ec14b781d2375bf725316c3fb0) // quotient_const + mstore(add(payload, 0x1280), 0x000000000000000000000000000000259007b94eb27289dcc84c1f9dce2e9860) // quotient_const + mstore(add(payload, 0x12a0), 0x73eda753299d7d483339d80809a1d79d35602792ebdf9e00793f578beeb3c47d) // quotient_const + mstore(add(payload, 0x12c0), 0x73eda753299d7d483339d80809a1d802ddd0f5801766ac88a72f1a40a8fff9c1) // quotient_const + mstore(add(payload, 0x12e0), 0x73eda753299d7d483339d80809a1d7abe053165ef916860e42e15847ef869571) // quotient_const + mstore(add(payload, 0x1300), 0x73eda753299d7d483339d80809a1d7ec490dd323de5caac125229595cbe0efc1) // quotient_const + mstore(add(payload, 0x1320), 0x08a75c054be451b1f2ad6bd569f92577dd4bd64d6d5c968569fbf9fbe04d544d) // quotient_const + mstore(add(payload, 0x1340), 0x0000000000000000000000000000000000000000000000000000001800000000) // quotient_const + mstore(add(payload, 0x1360), 0x0000000000000000000000000000000000000000180000000000000000000000) // quotient_const + mstore(add(payload, 0x1380), 0x0000000000000000000000000000000000000000000000000000000000006000) // quotient_const + mstore(add(payload, 0x13a0), 0x0000000000000000000000000000000000000000000000600000000000000000) // quotient_const + mstore(add(payload, 0x13c0), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffff700000001) // quotient_const + mstore(add(payload, 0x13e0), 0x73eda753299d7d483339d80809a1d80553bda402f7fe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x1400), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffe001) // quotient_const + mstore(add(payload, 0x1420), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bdeffffffff00000001) // quotient_const + mstore(add(payload, 0x1440), 0x73eda753299d7d483339d80809a1d7e355af567743ae3bbda4ffd260212ddbdb) // quotient_const + mstore(add(payload, 0x1460), 0x73eda753299d7d483339d80809a1d7ac8bb094e10dd00bb871cf13341ce9f671) // quotient_const + mstore(add(payload, 0x1480), 0x73eda753299d7d483339d80809a1d7b70b86399be46147106cd7aef7d718818d) // quotient_const + mstore(add(payload, 0x14a0), 0x73eda753299d7d483339d80809a1d7ae5b2956bf70a17c7596cc5f626b73876b) // quotient_const + mstore(add(payload, 0x14c0), 0x73eda753299d7d483339d80809a1d7b6bc3584645b26895898ae7ba60d735ad1) // quotient_const + mstore(add(payload, 0x14e0), 0x73eda753299d7d483339d80809a1d7b095efed6fd9f96e39d8c5c777a6fa1179) // quotient_const + mstore(add(payload, 0x1500), 0x00000000000000000000000000000065fa2ae8a334f060c4110088dc9c766c72) // quotient_const + mstore(add(payload, 0x1520), 0x00000000000000000000000000000000000000000c0000000000000000000000) // quotient_const + mstore(add(payload, 0x1540), 0x0000000000000000000000000000010a58272d65d68af0d3aa92c660a9421cb0) // quotient_const + mstore(add(payload, 0x1560), 0x0000000000000000000000000000000000000000000000300000000000000000) // quotient_const + mstore(add(payload, 0x1580), 0x000000000000000000000000000000ead8a63f3552d73ecbb978f3157ab67b5c) // quotient_const + mstore(add(payload, 0x15a0), 0x000000000000000000000000000000852c1396b2eb457869d549633054a10e58) // quotient_const + mstore(add(payload, 0x15c0), 0x00000000000000000000000000000104e9bce7caae169e9c3b9ae1d5bda569c2) // quotient_const + mstore(add(payload, 0x15e0), 0x0000000000000000000000000000008274de73e5570b4f4e1dcd70eaded2b4e1) // quotient_const + mstore(add(payload, 0x1600), 0x000000000000000000000000000000ebc6985edbee8777f335f48d0ad7a5ef90) // quotient_const + mstore(add(payload, 0x1620), 0x0000000000000000000000000000007f1cb491dcb90764a7bad754cb0588e5cc) // quotient_const + mstore(add(payload, 0x1640), 0x73eda753299d7d48333049095fbd120c6b5942dd2166802b5297f978232a0002) // quotient_const + mstore(add(payload, 0x1660), 0x0000000000000000000006000000000000000000000000000000000000000000) // quotient_const + mstore(add(payload, 0x1680), 0x0000000600000000000000000000000000000000000000000000000000000000) // quotient_const + mstore(add(payload, 0x16a0), 0x4302515f88a4431e1fbaccbc5adc8f25703b5745de78f77d0d3fe37cf2c01c83) // quotient_const + mstore(add(payload, 0x16c0), 0x140e70dbca64831b4b8f40317b68cd20f34ec27e98adf994cf555b0db316abbd) // quotient_const + mstore(add(payload, 0x16e0), 0x73eda753299d7d483339d60809a1d80553bda402fffe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x1700), 0x73eda751299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x1720), 0x104e71fbe05313635bd503c3e4ec6d9ff680c43f0b2c76d5fb955ed6046aa12a) // quotient_const + mstore(add(payload, 0x1740), 0x1ff51227ca6853644d38dd4784bdae4c2024f5d6cd1acb78bae38c506ef8716c) // quotient_const + mstore(add(payload, 0x1760), 0x70156f48f76cc14b27137d78d0b4371477819d08e9f2c77036ebc744ad6da6cb) // quotient_const + mstore(add(payload, 0x1780), 0x37be870795549c37dcd00b9588085d0fa1ab8c1ad655e52c23ed8949f0fb5ae3) // quotient_const + mstore(add(payload, 0x17a0), 0x62a9cec91e3168b1496ccfcf27ad7a8d3c8d65793936323648c2b29cb6a41ac8) // quotient_const + mstore(add(payload, 0x17c0), 0x6ed3edb47fe320414c6b5a76f4cc0bce626d1a256829e20dfb44af3c3a5c88b0) // quotient_const + mstore(add(payload, 0x17e0), 0x62ea7e5a34aa00528575fab42c44f48dc6c3a5a2fadad34f0a31529dec7b61e0) // quotient_const + mstore(add(payload, 0x1800), 0x6de98a7ebdd251f08ee9668a33e94c65bdb3185246bd05a64c5767be80a6f6b3) // quotient_const + mstore(add(payload, 0x1820), 0x0b88a81e969233f724730fadaac8e2d294b414ee4222bdac5b3caa2ef7b70ba2) // quotient_const + mstore(add(payload, 0x1840), 0x0000000300000000000000000000000000000000000000000000000000000000) // quotient_const + mstore(add(payload, 0x1860), 0x409fb98f933d25e8d0038d4f7b2a98dbc278a3b57cfb0879943764202d0def59) // quotient_const + mstore(add(payload, 0x1880), 0x43fe0c177a010031bf648c1cc285529323863340cc562ac9e7aaad86598b55df) // quotient_const + mstore(add(payload, 0x18a0), 0x33cb899e22443dc4bd6718aaa5dd18684590bb9d54587d5a25b7e826dc13afab) // quotient_const + mstore(add(payload, 0x18c0), 0x5a46b0715e6d5198819eb2abc26638708b1b23dc3e7cb23c4a1bb20f9686f7ad) // quotient_const + mstore(add(payload, 0x18e0), 0x0f4d2cdbfd2f1714b46b78b33e8164a4d3f19d98c77d6dd30e31f24850ea65f3) // quotient_const + mstore(add(payload, 0x1900), 0x419d6a1793664a2e73d2a85da4119e5513d7a0cde3bde4e90718f923a87532fa) // quotient_const + mstore(add(payload, 0x1920), 0x33097aeadeda76e1094b97fb9816aa66a6edfb200f6a9a0fe16c08233a8dda63) // quotient_const + mstore(add(payload, 0x1940), 0x09062b3ea1b0c1037678aa3cc094d16f610fd18915e201850d7ce460bf058df5) // quotient_const + mstore(add(payload, 0x1960), 0x0456a29a706afacf2158850711006fe0acf6a437e9477bf6f782dfac86f2cf7b) // quotient_const + mstore(add(payload, 0x1980), 0x0397cc06bc030aab970dabe70cd498bbeea8daaea65607bb6a872125fec74a10) // quotient_const + mstore(add(payload, 0x19a0), 0x73eda753299d7d4833351088b4af7508df8b737010b26e15294bfcbb91950003) // quotient_const + mstore(add(payload, 0x19c0), 0x5e1d3dbecda6214343e24a47f45c5d033197ad01b65a730af95dc57e90c49140) // quotient_const + mstore(add(payload, 0x19e0), 0x6bd72f9cfc53af9d931896e77ea5c61244cb6d5fae8954f37dc7b9002f5aa78a) // quotient_const + mstore(add(payload, 0x1a00), 0x4997c5aa3a5fa07bcaf880a9054bef831effbd9cd58e46d9bb4fb88ef99de0db) // quotient_const + mstore(add(payload, 0x1a20), 0x058560108a8005860008060d000b0200011b0000210201030700020000852002) // quotient_program + mstore(add(payload, 0x1a40), 0x85400385600485800585a00085c00285e0038600068620078640088660098680) // quotient_program + mstore(add(payload, 0x1a60), 0x0a86a00b86c00c86e00d87000e87200487400587600687800787a085200085c0) // quotient_program + mstore(add(payload, 0x1a80), 0x0285e00386000487400587600687800787a085400285c00385e0048600058740) // quotient_program + mstore(add(payload, 0x1aa0), 0x0687600787800f87a085600385c00485e00586000687400787600f87801087a0) // quotient_program + mstore(add(payload, 0x1ac0), 0x85800485c00585e00686000787400f87601087801187a085a00585c00685e007) // quotient_program + mstore(add(payload, 0x1ae0), 0x86000f87401087601187801287a086200685c00785e00f860010874011876012) // quotient_program + mstore(add(payload, 0x1b00), 0x87801387a086400785c00f85e01086001187401287601387801487a01587c016) // quotient_program + mstore(add(payload, 0x1b20), 0x88000b03000121021703070001000085200285400385601885801985a00085c0) // quotient_program + mstore(add(payload, 0x1b40), 0x0285e00386001a86201b86400886600986800a86a01c86c01d86e01e87001f87) // quotient_program + mstore(add(payload, 0x1b60), 0x201887401987601a87801b87a085200085c00285e00386001887401987601a87) // quotient_program + mstore(add(payload, 0x1b80), 0x801b87a085400285c00385e01886001987401a87601b87802087a085600385c0) // quotient_program + mstore(add(payload, 0x1ba0), 0x1885e01986001a87401b87602087802187a085801885c01985e01a86001b8740) // quotient_program + mstore(add(payload, 0x1bc0), 0x2087602187802287a085a01985c01a85e01b86002087402187602287802387a0) // quotient_program + mstore(add(payload, 0x1be0), 0x86201a85c01b85e02086002187402287602387802487a086401b85c02085e021) // quotient_program + mstore(add(payload, 0x1c00), 0x86002287402387602487802587a02687c00b0300011b00012102270100000900) // quotient_program + mstore(add(payload, 0x1c20), 0x0686200786400886600986800a86a00b86c00c86e00085200285400385600485) // quotient_program + mstore(add(payload, 0x1c40), 0x800585a00d87000e87201587c01688000b04000121022801000008001a86201b) // quotient_program + mstore(add(payload, 0x1c60), 0x86400886600986800a86a01c86c01d86e00085200285400385601885801985a0) // quotient_program + mstore(add(payload, 0x1c80), 0x1e87001f87202687c00b04000121038820290000000b2b0885200985400a8560) // quotient_program + mstore(add(payload, 0x1ca0), 0x2a85c02b85e02c86000886600986800a86a02d87c02e87e00885208660098520) // quotient_program + mstore(add(payload, 0x1cc0), 0x86800a852086a009854086600a854086802f854087200a856086602f85608700) // quotient_program + mstore(add(payload, 0x1ce0), 0x30856087202f858086e0308580870031858087202f85a086c03085a086e03185) // quotient_program + mstore(add(payload, 0x1d00), 0xa087003285a087200085c085c02b85c085e02c85c086000385e085e03385e087) // quotient_program + mstore(add(payload, 0x1d20), 0xa0338600878034860087a02f862086a030862086c031862086e0328620870035) // quotient_program + mstore(add(payload, 0x1d40), 0x862087202f8640868030864086a031864086c032864086e03586408700368640) // quotient_program + mstore(add(payload, 0x1d60), 0x87203387408760348740878037874087a03887608760378760878039876087a0) // quotient_program + mstore(add(payload, 0x1d80), 0x3a878087803b878087a03c87a087a00d000b050000210388203d030800021508) // quotient_program + mstore(add(payload, 0x1da0), 0x85200985400a85600b85800c85a02a85c02b85e02c86000d86200e8640088660) // quotient_program + mstore(add(payload, 0x1dc0), 0x0986800a86a00b86c00c86e00d87000e87203e87403f87604087804187a08520) // quotient_program + mstore(add(payload, 0x1de0), 0x0886600986800a86a00b86c00c86e00d87000e872085400986600a86800b86a0) // quotient_program + mstore(add(payload, 0x1e00), 0x0c86c00d86e00e870042872085600a86600b86800c86a00d86c00e86e0428700) // quotient_program + mstore(add(payload, 0x1e20), 0x43872085800b86600c86800d86a00e86c04286e043870044872085a00c86600d) // quotient_program + mstore(add(payload, 0x1e40), 0x86800e86a04286c04386e044870045872085c00085c02b85e02c86003e87403f) // quotient_program + mstore(add(payload, 0x1e60), 0x87604087804187a086200d86600e86804286a04386c04486e045870046872086) // quotient_program + mstore(add(payload, 0x1e80), 0x400e86604286804386a04486c04586e04687004787201587c01688000385e085) // quotient_program + mstore(add(payload, 0x1ea0), 0xe03e85e086003f85e087404085e087604185e087804885e087a0058600860040) // quotient_program + mstore(add(payload, 0x1ec0), 0x860087404186008760488600878049860087a007874087404887408760498740) // quotient_program + mstore(add(payload, 0x1ee0), 0x87804a874087a010876087604a876087804b876087a012878087804c878087a0) // quotient_program + mstore(add(payload, 0x1f00), 0x1487a087a00d000b050001210388204d03080001150885200985400a85601c85) // quotient_program + mstore(add(payload, 0x1f20), 0x801d85a02a85c02b85e02c86001e86201f86400886600986800a86a01c86c01d) // quotient_program + mstore(add(payload, 0x1f40), 0x86e01e87001f87204e87404f87605087805187a085200886600986800a86a01c) // quotient_program + mstore(add(payload, 0x1f60), 0x86c01d86e01e87001f872085400986600a86801c86a01d86c01e86e01f870052) // quotient_program + mstore(add(payload, 0x1f80), 0x872085600a86601c86801d86a01e86c01f86e052870053872085801c86601d86) // quotient_program + mstore(add(payload, 0x1fa0), 0x801e86a01f86c05286e053870054872085a01d86601e86801f86a05286c05386) // quotient_program + mstore(add(payload, 0x1fc0), 0xe054870055872085c00085c02b85e02c86004e87404f87605087805187a08620) // quotient_program + mstore(add(payload, 0x1fe0), 0x1e86601f86805286a05386c05486e055870056872086401f86605286805386a0) // quotient_program + mstore(add(payload, 0x2000), 0x5486c05586e05687005787202687c00385e085e04e85e086004f85e087405085) // quotient_program + mstore(add(payload, 0x2020), 0xe087605185e087805885e087a019860086005086008740518600876058860087) // quotient_program + mstore(add(payload, 0x2040), 0x8059860087a01b87408740588740876059874087805a874087a021876087605a) // quotient_program + mstore(add(payload, 0x2060), 0x876087805b876087a023878087805c878087a02587a087a00d000b0500012103) // quotient_program + mstore(add(payload, 0x2080), 0x88205d0000000c390885200985400a85602d87c02e87e0008820008840028860) // quotient_program + mstore(add(payload, 0x20a0), 0x0388800889200989400a89600085c088400285c088600385c088800885c08920) // quotient_program + mstore(add(payload, 0x20c0), 0x0985c089400a85c089600285e088400385e088605e85e089000985e089200a85) // quotient_program + mstore(add(payload, 0x20e0), 0xe089402f85e089e003860088405e860088e038860089000a860089202f860089) // quotient_program + mstore(add(payload, 0x2100), 0xc030860089e0008660882002868088200386a088205e874088c038874088e05f) // quotient_program + mstore(add(payload, 0x2120), 0x874089002f874089a030874089c031874089e05e876088a038876088c05f8760) // quotient_program + mstore(add(payload, 0x2140), 0x88e03a876089002f8760898030876089a031876089c032876089e05e87808880) // quotient_program + mstore(add(payload, 0x2160), 0x38878088a05f878088c03a878088e060878089002f8780896030878089803187) // quotient_program + mstore(add(payload, 0x2180), 0x8089a032878089c035878089e05e87a088603887a088805f87a088a03a87a088) // quotient_program + mstore(add(payload, 0x21a0), 0xc06087a088e03c87a089002f87a089403087a089603187a089803287a089a035) // quotient_program + mstore(add(payload, 0x21c0), 0x87a089c03687a089e00d000b0600002103882061020f000a0016880000882000) // quotient_program + mstore(add(payload, 0x21e0), 0x88400288600388800488a00588c00688e00789000889200989400a89600b8980) // quotient_program + mstore(add(payload, 0x2200), 0x0c89a088200086600286800386a00486c00586e006870007872088400085c002) // quotient_program + mstore(add(payload, 0x2220), 0x85e00386000487400587600687800787a088600285c00385e004860005874006) // quotient_program + mstore(add(payload, 0x2240), 0x87600787800f87a088800385c00485e00586000687400787600f87801087a088) // quotient_program + mstore(add(payload, 0x2260), 0xa00485c00585e00686000787400f87601087801187a088c00585c00685e00786) // quotient_program + mstore(add(payload, 0x2280), 0x000f87401087601187801287a088e00685c00785e00f86001087401187601287) // quotient_program + mstore(add(payload, 0x22a0), 0x801387a089000785c00f85e01086001187401287601387801487a085c0088920) // quotient_program + mstore(add(payload, 0x22c0), 0x0989400a89600b89800c89a00d89c00e89e085e00989200a89400b89600c8980) // quotient_program + mstore(add(payload, 0x22e0), 0x0d89a00e89c04289e086000a89200b89400c89600d89800e89a04289c04389e0) // quotient_program + mstore(add(payload, 0x2300), 0x87400b89200c89400d89600e89804289a04389c04489e087600c89200d89400e) // quotient_program + mstore(add(payload, 0x2320), 0x89604289804389a04489c04589e087800d89200e89404289604389804489a045) // quotient_program + mstore(add(payload, 0x2340), 0x89c04689e087a00e89204289404389604489804589a04689c04789e008852009) // quotient_program + mstore(add(payload, 0x2360), 0x85400a85600b85800c85a00d86200e86401587c00d89c00e89e00d000b060001) // quotient_program + mstore(add(payload, 0x2380), 0x2103882062020f0009000088200088400288600388801888a01988c01a88e01b) // quotient_program + mstore(add(payload, 0x23a0), 0x89000889200989400a89601c89801d89a01e89c088200086600286800386a018) // quotient_program + mstore(add(payload, 0x23c0), 0x86c01986e01a87001b872088400085c00285e00386001887401987601a87801b) // quotient_program + mstore(add(payload, 0x23e0), 0x87a088600285c00385e01886001987401a87601b87802087a088800385c01885) // quotient_program + mstore(add(payload, 0x2400), 0xe01986001a87401b87602087802187a088a01885c01985e01a86001b87402087) // quotient_program + mstore(add(payload, 0x2420), 0x602187802287a088c01985c01a85e01b86002087402187602287802387a088e0) // quotient_program + mstore(add(payload, 0x2440), 0x1a85c01b85e02086002187402287602387802487a089001b85c02085e0218600) // quotient_program + mstore(add(payload, 0x2460), 0x2287402387602487802587a085c00889200989400a89601c89801d89a01e89c0) // quotient_program + mstore(add(payload, 0x2480), 0x1f89e085e00989200a89401c89601d89801e89a01f89c05289e086000a89201c) // quotient_program + mstore(add(payload, 0x24a0), 0x89401d89601e89801f89a05289c05389e087401c89201d89401e89601f898052) // quotient_program + mstore(add(payload, 0x24c0), 0x89a05389c05489e087601d89201e89401f89605289805389a05489c05589e087) // quotient_program + mstore(add(payload, 0x24e0), 0x801e89201f89405289605389805489a05589c05689e087a01f89205289405389) // quotient_program + mstore(add(payload, 0x2500), 0x605489805589a05689c05789e00885200985400a85601c85801d85a01e86201f) // quotient_program + mstore(add(payload, 0x2520), 0x86402687c01f89e00d000b06000121038820630000000b2b6485206585406685) // quotient_program + mstore(add(payload, 0x2540), 0x606785c06885e06986006786606886806986a02d87c02e87e06a852085206585) // quotient_program + mstore(add(payload, 0x2560), 0x20854066852085606b854085406c854086406c856086206d856086406c858085) // quotient_program + mstore(add(payload, 0x2580), 0xa06d858086206e858086406f85a085a06e85a086207085a086406785c0866068) // quotient_program + mstore(add(payload, 0x25a0), 0x85c086806985c086a06885e086606985e086807185e087206986008660718600) // quotient_program + mstore(add(payload, 0x25c0), 0x8700728600872073862086207486208640758640864071868087a07186a08780) // quotient_program + mstore(add(payload, 0x25e0), 0x7286a087a07186c087607286c087807686c087a07186e087407286e087607686) // quotient_program + mstore(add(payload, 0x2600), 0xe087807786e087a072870087407687008760778700878078870087a076872087) // quotient_program + mstore(add(payload, 0x2620), 0x407787208760788720878079872087a00d000b070000210388207a0308000215) // quotient_program + mstore(add(payload, 0x2640), 0x6485206585406685607b85807c85a06785c06885e06986007d86207e86406786) // quotient_program + mstore(add(payload, 0x2660), 0x606886806986a07f86c08086e08187008287207f87408087608187808287a085) // quotient_program + mstore(add(payload, 0x2680), 0x206a85206585406685607b85807c85a07d86207e864085c06786606886806986) // quotient_program + mstore(add(payload, 0x26a0), 0xa07f86c08086e081870082872085e06886606986807f86a08086c08186e08287) // quotient_program + mstore(add(payload, 0x26c0), 0x0083872086006986607f86808086a08186c08286e083870084872087407f8660) // quotient_program + mstore(add(payload, 0x26e0), 0x8086808186a08286c08386e084870085872087608086608186808286a08386c0) // quotient_program + mstore(add(payload, 0x2700), 0x8486e085870086872087808186608286808386a08486c08586e0868700878720) // quotient_program + mstore(add(payload, 0x2720), 0x87a08286608386808486a08586c08686e08787008887201587c01688006b8540) // quotient_program + mstore(add(payload, 0x2740), 0x85407b854085607c854085807d854085a07e8540862089854086408a85608560) // quotient_program + mstore(add(payload, 0x2760), 0x7d856085807e856085a089856086208b856086408c8580858089858085a08b85) // quotient_program + mstore(add(payload, 0x2780), 0x8086208d858086408e85a085a08d85a086208f85a08640908620862091862086) // quotient_program + mstore(add(payload, 0x27a0), 0x4092864086400d000b0700012103882093030800011564852065854066856094) // quotient_program + mstore(add(payload, 0x27c0), 0x85809585a06785c06885e06986009686209786406786606886806986a09886c0) // quotient_program + mstore(add(payload, 0x27e0), 0x9986e09a87009b87209887409987609a87809b87a085206a8520658540668560) // quotient_program + mstore(add(payload, 0x2800), 0x9485809585a096862097864085c06786606886806986a09886c09986e09a8700) // quotient_program + mstore(add(payload, 0x2820), 0x9b872085e06886606986809886a09986c09a86e09b87009c8720860069866098) // quotient_program + mstore(add(payload, 0x2840), 0x86809986a09a86c09b86e09c87009d872087409886609986809a86a09b86c09c) // quotient_program + mstore(add(payload, 0x2860), 0x86e09d87009e872087609986609a86809b86a09c86c09d86e09e87009f872087) // quotient_program + mstore(add(payload, 0x2880), 0x809a86609b86809c86a09d86c09e86e09f8700a0872087a09b86609c86809d86) // quotient_program + mstore(add(payload, 0x28a0), 0xa09e86c09f86e0a08700a187202687c06b854085409485408560958540858096) // quotient_program + mstore(add(payload, 0x28c0), 0x854085a09785408620a285408640a385608560968560858097856085a0a28560) // quotient_program + mstore(add(payload, 0x28e0), 0x8620a485608640a585808580a2858085a0a485808620a685808640a785a085a0) // quotient_program + mstore(add(payload, 0x2900), 0xa685a08620a885a08640a986208620aa86208640ab864086400d000b07000121) // quotient_program + mstore(add(payload, 0x2920), 0x038820ac0000000e100085200285400385606785c06885e06986000086600286) // quotient_program + mstore(add(payload, 0x2940), 0x800386a02d87c02e87e00088400288600388800885c085c06885c085e06985c0) // quotient_program + mstore(add(payload, 0x2960), 0x86000a85e085e07185e087a0718600878072860087a071874087607287408780) // quotient_program + mstore(add(payload, 0x2980), 0x76874087a03087608760768760878077876087a0328780878078878087a03687) // quotient_program + mstore(add(payload, 0x29a0), 0xa087a00d000b08000021038820ad040100021500852002854003856004858005) // quotient_program + mstore(add(payload, 0x29c0), 0x85a06785c06885e06986000686200786400086600286800386a00486c00586e0) // quotient_program + mstore(add(payload, 0x29e0), 0x0687000787207f87408087608187808287a00088400288600388800488a00588) // quotient_program + mstore(add(payload, 0x2a00), 0xc00688e007890085c00885c06885e06986007f87408087608187808287a01587) // quotient_program + mstore(add(payload, 0x2a20), 0xc01688000a85e085e07f85e086008085e087408185e087608285e087808385e0) // quotient_program + mstore(add(payload, 0x2a40), 0x87a00c8600860081860087408286008760838600878084860087a00e87408740) // quotient_program + mstore(add(payload, 0x2a60), 0x8387408760848740878085874087a04387608760858760878086876087a04587) // quotient_program + mstore(add(payload, 0x2a80), 0x80878087878087a04787a087a00d000b08000121038820ae0401000115008520) // quotient_program + mstore(add(payload, 0x2aa0), 0x0285400385601885801985a06785c06885e06986001a86201b86400086600286) // quotient_program + mstore(add(payload, 0x2ac0), 0x800386a01886c01986e01a87001b87209887409987609a87809b87a000884002) // quotient_program + mstore(add(payload, 0x2ae0), 0x88600388801888a01988c01a88e01b890085c00885c06885e069860098874099) // quotient_program + mstore(add(payload, 0x2b00), 0x87609a87809b87a02687c00a85e085e09885e086009985e087409a85e087609b) // quotient_program + mstore(add(payload, 0x2b20), 0x85e087809c85e087a01d860086009a860087409b860087609c860087809d8600) // quotient_program + mstore(add(payload, 0x2b40), 0x87a01f874087409c874087609d874087809e874087a053876087609e87608780) // quotient_program + mstore(add(payload, 0x2b60), 0x9f876087a05587808780a0878087a05787a087a00d000b080001058520118520) // quotient_program + mstore(add(payload, 0x2b80), 0x11852005858008060d000b0900000585401185401185400585a008060d000b09) // quotient_program + mstore(add(payload, 0x2ba0), 0x000105856011856011856005862008060d000b0900011b00021b000305860008) // quotient_program + mstore(add(payload, 0x2bc0), 0x108b200585201185201185800daf060585401185401185a00db0060585601185) // quotient_program + mstore(add(payload, 0x2be0), 0x601186200db1060d000b090001191f0000000000000000000000000000000000) // quotient_program + // Fixed-column commitment 0, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2c00), 0x0000000000000000000000000000000016e98a681dca730dcbe651aec5493976) // fixed_comms[0].x_hi + mstore(add(payload, 0x2c20), 0x11944be61932e7d19a63786871335939b0eefa7e32507b68b4ad6fd89a5c1028) // fixed_comms[0].x_lo + mstore(add(payload, 0x2c40), 0x0000000000000000000000000000000010e6ae8d3becb251e60db7d788be7029) // fixed_comms[0].y_hi + mstore(add(payload, 0x2c60), 0x8dbcdfdbb394da2ca9725e07485d6b630569a66ec867aa0d605573ae82d550df) // fixed_comms[0].y_lo + // Fixed-column commitment 1, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2c80), 0x000000000000000000000000000000000fb2ef3076aa1a8068bec7ef35167c1c) // fixed_comms[1].x_hi + mstore(add(payload, 0x2ca0), 0xf1f26a24abaa3b4dc8c6d218021c08429e4c175654b5996eb4de74d17a5c188d) // fixed_comms[1].x_lo + mstore(add(payload, 0x2cc0), 0x00000000000000000000000000000000000f94fa3dcc144ffe6a4ace1de5f956) // fixed_comms[1].y_hi + mstore(add(payload, 0x2ce0), 0x76ad02ffc32c0345767b466f064aec3f1101ecd9eaf91ba5d4c145f792d1285a) // fixed_comms[1].y_lo + // Fixed-column commitment 2, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2d00), 0x0000000000000000000000000000000016909a26057dec4152b5bd207c37f9a8) // fixed_comms[2].x_hi + mstore(add(payload, 0x2d20), 0xc3af2c8cfa63dfd83e34704255392445fae3c47d749e435e0291232659a8caeb) // fixed_comms[2].x_lo + mstore(add(payload, 0x2d40), 0x0000000000000000000000000000000000b9edb176686ec391a3684928dc0842) // fixed_comms[2].y_hi + mstore(add(payload, 0x2d60), 0x80c60eca091b8a3925e31ef1032793f2ef4e31225ab560bf721f17525dd476c8) // fixed_comms[2].y_lo + // Fixed-column commitment 3, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2d80), 0x0000000000000000000000000000000014861e20b064f637d60cc250ff3b8804) // fixed_comms[3].x_hi + mstore(add(payload, 0x2da0), 0x9b9697b5245d031c784398bdf950e99d7968c86e92c4ec1cb3821a69686dfb09) // fixed_comms[3].x_lo + mstore(add(payload, 0x2dc0), 0x000000000000000000000000000000000c117b524f84a06b024a40003e1c3aab) // fixed_comms[3].y_hi + mstore(add(payload, 0x2de0), 0x25253a3cf5b53d2f77d1f9328a9c0b32bea4f591eb470dbb599b5fbdfa7df46b) // fixed_comms[3].y_lo + // Fixed-column commitment 4, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2e00), 0x000000000000000000000000000000001420bb2d8c182dda6c982eb820e035f6) // fixed_comms[4].x_hi + mstore(add(payload, 0x2e20), 0xa4a5c6c69376dfda17ac5588d34a0b313800b8d18ecd818f3e4e2671589ec691) // fixed_comms[4].x_lo + mstore(add(payload, 0x2e40), 0x0000000000000000000000000000000000855eb6333071d5bb90a0351dbf958c) // fixed_comms[4].y_hi + mstore(add(payload, 0x2e60), 0x13e5648bcafb55776c78e46031e4620cfda02728e26bb97f067a9a0cdf72aa9f) // fixed_comms[4].y_lo + // Fixed-column commitment 5, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2e80), 0x000000000000000000000000000000000707698990b767903c9aaf028c98b46a) // fixed_comms[5].x_hi + mstore(add(payload, 0x2ea0), 0xc4925e4e30db42105aade1a5cd7dd9b3d2222bd7085071b5c2212709cfcc2d46) // fixed_comms[5].x_lo + mstore(add(payload, 0x2ec0), 0x000000000000000000000000000000000a88a7d4f1148bd3efffaedb5be62777) // fixed_comms[5].y_hi + mstore(add(payload, 0x2ee0), 0x27fad92f1005d28d12ef861b5dcf8b580a1322048d469da5f84b26fa7f8d2e4d) // fixed_comms[5].y_lo + // Fixed-column commitment 6, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2f00), 0x000000000000000000000000000000001495975f8f8c0cb6c54e1163fe045732) // fixed_comms[6].x_hi + mstore(add(payload, 0x2f20), 0x2be3eb4af919cb0eb6126028ebeab81d075dff85aa522989166bf6e01880c1be) // fixed_comms[6].x_lo + mstore(add(payload, 0x2f40), 0x0000000000000000000000000000000018bdd4eef361f4a6d23a6511b48e20fb) // fixed_comms[6].y_hi + mstore(add(payload, 0x2f60), 0x4080898637660f3f3371adfb48585dd2ad89c29cc260d604ce9cd68a31ff427d) // fixed_comms[6].y_lo + // Fixed-column commitment 7, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2f80), 0x00000000000000000000000000000000166c1cd47b07ac0aee553f0b4277cf37) // fixed_comms[7].x_hi + mstore(add(payload, 0x2fa0), 0xb4822422a23ea90486c60a5b3b9052dff113041d51718304e781a5a7bd93bdc2) // fixed_comms[7].x_lo + mstore(add(payload, 0x2fc0), 0x00000000000000000000000000000000016c91bfb3ce38ba23df779d69abeaa4) // fixed_comms[7].y_hi + mstore(add(payload, 0x2fe0), 0x8aaa142a59667ce4eabfaf36c5453af4aaec6e596ad923693a2a7d1483c2fd20) // fixed_comms[7].y_lo + // Fixed-column commitment 8, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3000), 0x000000000000000000000000000000001262dde7fc4aeca256572897f5d601ef) // fixed_comms[8].x_hi + mstore(add(payload, 0x3020), 0x5b8b5586b8593d43cbcd11d2fd80fe2a2214269e5479b5d976cf507d5262c273) // fixed_comms[8].x_lo + mstore(add(payload, 0x3040), 0x0000000000000000000000000000000012bd810c9b8eaaf899f161e2d8c26762) // fixed_comms[8].y_hi + mstore(add(payload, 0x3060), 0xc3f299919bfa2b2809a027cc9bcac1799ef2d48b11403ce47a5341f7307d2cd4) // fixed_comms[8].y_lo + // Fixed-column commitment 9, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3080), 0x00000000000000000000000000000000096d06362268ed80162468c5e664e2bb) // fixed_comms[9].x_hi + mstore(add(payload, 0x30a0), 0x1a1a3b60a6c419cb6edb470b646e9ed4e021686e93f82d0c3ee2448368fe81be) // fixed_comms[9].x_lo + mstore(add(payload, 0x30c0), 0x0000000000000000000000000000000015ceb98a36a3576ad6db1163a0a98d2a) // fixed_comms[9].y_hi + mstore(add(payload, 0x30e0), 0x0535a28ca330834bb07c6682d32b20cd8171f4c0d9f063d768b6e3185b6ae44e) // fixed_comms[9].y_lo + // Fixed-column commitment 10, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3100), 0x0000000000000000000000000000000019731a2f803950b978082f57fdfffb38) // fixed_comms[10].x_hi + mstore(add(payload, 0x3120), 0x0de3c6bef55492c30e409d77ac9acb523958dea2856b12cb4e87608584ab499a) // fixed_comms[10].x_lo + mstore(add(payload, 0x3140), 0x0000000000000000000000000000000005f057d8c91984630d36a0df6c2ebb07) // fixed_comms[10].y_hi + mstore(add(payload, 0x3160), 0x3881713eb294d8cdf7bb0374cb722b7bb5080101a54638216ab4886f0276a475) // fixed_comms[10].y_lo + // Fixed-column commitment 11, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3180), 0x0000000000000000000000000000000004d63c6ffcf22c5a45cbc67e299c2b3d) // fixed_comms[11].x_hi + mstore(add(payload, 0x31a0), 0xcfd98199c525785d127006aca0f82774876a94e120b9bde61b9227ee96dc6841) // fixed_comms[11].x_lo + mstore(add(payload, 0x31c0), 0x0000000000000000000000000000000002385e4a7b9ab571a73f07d9574152e5) // fixed_comms[11].y_hi + mstore(add(payload, 0x31e0), 0xa45de038325c587523413d98b5feb6d2071c293abbeab53bf941d76b264f3369) // fixed_comms[11].y_lo + // Fixed-column commitment 12, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3200), 0x000000000000000000000000000000000eea9ee53f693242e50678f2ac4a5053) // fixed_comms[12].x_hi + mstore(add(payload, 0x3220), 0x6bc4aa60deae574c46e3cf56a8d14af17d603f8c222728fb09300eee5c5b326b) // fixed_comms[12].x_lo + mstore(add(payload, 0x3240), 0x0000000000000000000000000000000014aa3a043bb1340472428a1756ad82bb) // fixed_comms[12].y_hi + mstore(add(payload, 0x3260), 0x49fa2fee5ff945799006e851969947efb6351c7642b2683deb31537ea6209643) // fixed_comms[12].y_lo + // Fixed-column commitment 13, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3280), 0x000000000000000000000000000000000b7028c202ee5b6a74d6ccfe5990dc98) // fixed_comms[13].x_hi + mstore(add(payload, 0x32a0), 0x4381d45aa22ccb019d848829dd72d16143e5af158899fcc009d5ca17c97c3414) // fixed_comms[13].x_lo + mstore(add(payload, 0x32c0), 0x0000000000000000000000000000000006c6ca7119058dbb54d5302f125f9cef) // fixed_comms[13].y_hi + mstore(add(payload, 0x32e0), 0xdc6b68957c7b970427a4b5a3bd0aa7ff5fa033687afdc2a4e150a408b0a5f4d2) // fixed_comms[13].y_lo + // Fixed-column commitment 14, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3300), 0x0000000000000000000000000000000008a540ee790774da565637ec30ced988) // fixed_comms[14].x_hi + mstore(add(payload, 0x3320), 0x563c90d314d01996465fa3f893d5fb010b617a4edcabc5c8f8141584792a0067) // fixed_comms[14].x_lo + mstore(add(payload, 0x3340), 0x00000000000000000000000000000000071e81991f7c8f62be7df37cd485cb5e) // fixed_comms[14].y_hi + mstore(add(payload, 0x3360), 0x1f941d942cc15f814350867bcdf1521ef6e7e76b6928827688b0d3d14a6cf1a3) // fixed_comms[14].y_lo + // Fixed-column commitment 15, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3380), 0x0000000000000000000000000000000005a4ed142d6c05e535a91ff8244ca5cb) // fixed_comms[15].x_hi + mstore(add(payload, 0x33a0), 0xdda66355e156c19ea759a1f5aecd1225e16cb3f18397a66b055a2918a2a74d54) // fixed_comms[15].x_lo + mstore(add(payload, 0x33c0), 0x000000000000000000000000000000000ce5eb9724134cce6feb1c0ff1b66ab3) // fixed_comms[15].y_hi + mstore(add(payload, 0x33e0), 0x5e79b2b4087200f1d9a76ee4a2b3b5424266da9d518653e9391e9a94196f1d7f) // fixed_comms[15].y_lo + // Fixed-column commitment 16, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3400), 0x0000000000000000000000000000000009b07c1df13d348c1ed7cb560fcdd682) // fixed_comms[16].x_hi + mstore(add(payload, 0x3420), 0x55221aadb75410fa4526857c95cea6bf3a0efa98e31898de56ea7ff30f7c514a) // fixed_comms[16].x_lo + mstore(add(payload, 0x3440), 0x0000000000000000000000000000000011b2d20f8b12527aba78064c809753e2) // fixed_comms[16].y_hi + mstore(add(payload, 0x3460), 0x21cebde9bb41aa67abae156e3a179655388e51211ba5614fb696a7f0cc212560) // fixed_comms[16].y_lo + // Fixed-column commitment 17, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3480), 0x0000000000000000000000000000000010ce280498c90ca7501427caf6bcd6d9) // fixed_comms[17].x_hi + mstore(add(payload, 0x34a0), 0xb4987380f8d9e8ef710b81034a460c8da32362568e1e9fd6b2cb5297805c98ad) // fixed_comms[17].x_lo + mstore(add(payload, 0x34c0), 0x000000000000000000000000000000000292967ae2e91be4d3555cddb84538dd) // fixed_comms[17].y_hi + mstore(add(payload, 0x34e0), 0x0edcb077ce0626f751bee2673ff6b24c182fd2ae99a0cd83881a81a3facfcb6e) // fixed_comms[17].y_lo + // Fixed-column commitment 18, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3500), 0x0000000000000000000000000000000017cf661d855e771f61cab52918bc03e5) // fixed_comms[18].x_hi + mstore(add(payload, 0x3520), 0x329761bebc6704041c0430c64bc2d589210a4250f57e9dec51a807d4d6926f31) // fixed_comms[18].x_lo + mstore(add(payload, 0x3540), 0x000000000000000000000000000000000f50cacb58bf9101f3d7926b2e26675d) // fixed_comms[18].y_hi + mstore(add(payload, 0x3560), 0x036c7cf34f335946dfc1d1a6e148de3da13cabfe8e7507f10baca11e07231413) // fixed_comms[18].y_lo + // Fixed-column commitment 19, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3580), 0x0000000000000000000000000000000016caf868eda0aeb753fd8ec3bee96cbe) // fixed_comms[19].x_hi + mstore(add(payload, 0x35a0), 0x3217eade0bae8e908b6cfe1f144518cbaec5bbbbc6e6641d82702b3dd5997985) // fixed_comms[19].x_lo + mstore(add(payload, 0x35c0), 0x000000000000000000000000000000000a35da0b540c40574d639e726b9c7e6d) // fixed_comms[19].y_hi + mstore(add(payload, 0x35e0), 0xb45e6cbefb614f14a1721bdbca2dfcb20aaeb64926032522ba3de23420dc1d87) // fixed_comms[19].y_lo + // Fixed-column commitment 20, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3600), 0x000000000000000000000000000000000b7f1691be78a38c579c2ac6bef18d54) // fixed_comms[20].x_hi + mstore(add(payload, 0x3620), 0xf2d63e634590931fda91cce7af80f7c589cc37a98870e127c93e059ff020df07) // fixed_comms[20].x_lo + mstore(add(payload, 0x3640), 0x0000000000000000000000000000000012582699af0164d4335252425d0251c4) // fixed_comms[20].y_hi + mstore(add(payload, 0x3660), 0xa1af4e259a3f65f0065b2b05127538a8434de1326c4920ee45da346bbbc7d293) // fixed_comms[20].y_lo + // Fixed-column commitment 21, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3680), 0x000000000000000000000000000000000e004abc0ac243147c480dca32186a34) // fixed_comms[21].x_hi + mstore(add(payload, 0x36a0), 0x78642aaafe922adde54e02ae93479fa0ae45b75ed0229c2f358a0473d17c09a5) // fixed_comms[21].x_lo + mstore(add(payload, 0x36c0), 0x0000000000000000000000000000000013e0dac8358fff676678a7b7d854aee3) // fixed_comms[21].y_hi + mstore(add(payload, 0x36e0), 0x3fda118cfb39d426f3418966ed06283adbd804fbe8cf51fd89c2f5c410919f48) // fixed_comms[21].y_lo + // Fixed-column commitment 22, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3700), 0x00000000000000000000000000000000168c0d6883d0d5b67d3a5944ee0616a3) // fixed_comms[22].x_hi + mstore(add(payload, 0x3720), 0xce7ac537685a26b678bab6b1b50115c32a5f90b6ca56cb8560a40964840893ce) // fixed_comms[22].x_lo + mstore(add(payload, 0x3740), 0x00000000000000000000000000000000197d467f656fc49a1c5119d7cd826cc2) // fixed_comms[22].y_hi + mstore(add(payload, 0x3760), 0xbe6345c67f108e7e44f8ebfcebea5366fdee85d69947c8f74f623165dc1029b5) // fixed_comms[22].y_lo + // Fixed-column commitment 23, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3780), 0x00000000000000000000000000000000133f78d1d5ad537d358f421d115bdb2f) // fixed_comms[23].x_hi + mstore(add(payload, 0x37a0), 0x44eff93e623017fb4a06006877bd8504b8b614c6c877d73de7cdc2c2e69a8998) // fixed_comms[23].x_lo + mstore(add(payload, 0x37c0), 0x00000000000000000000000000000000067b406b38e3895daddca2f64de3ee21) // fixed_comms[23].y_hi + mstore(add(payload, 0x37e0), 0x5c7961f1d2f1904f7e2aa60bc20559610c868810183505527e28eea6089d6bf8) // fixed_comms[23].y_lo + // Fixed-column commitment 24, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3800), 0x000000000000000000000000000000000bd679e559d6bf9f498928a3074a0709) // fixed_comms[24].x_hi + mstore(add(payload, 0x3820), 0x23c4cf1d5823b59316852fd6653fe003a4a2da9ba443e4640993524c79a6f56d) // fixed_comms[24].x_lo + mstore(add(payload, 0x3840), 0x000000000000000000000000000000000821077b092335b9f70fb79a424cca13) // fixed_comms[24].y_hi + mstore(add(payload, 0x3860), 0x4e823d96a4fdeaf4154ab7c5ef023844695a10e9bfdf314847ccaa40205f980a) // fixed_comms[24].y_lo + // Fixed-column commitment 25, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3880), 0x00000000000000000000000000000000041f7c88095a1bbf5759d88a7d109853) // fixed_comms[25].x_hi + mstore(add(payload, 0x38a0), 0x4b1661f3b1b9012866e17c7550a5b52861ac915beba38767c7aeb04c1df330ac) // fixed_comms[25].x_lo + mstore(add(payload, 0x38c0), 0x000000000000000000000000000000000f2e8cc95c27bf0235efa88fa70bb3eb) // fixed_comms[25].y_hi + mstore(add(payload, 0x38e0), 0x59a5ef2602a9b70c5af7b0db755f10a8e0ffc380b61a580c3300fda8d6077854) // fixed_comms[25].y_lo + // Fixed-column commitment 26, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3900), 0x000000000000000000000000000000001983933b9a69c960c9eda1f5942af11a) // fixed_comms[26].x_hi + mstore(add(payload, 0x3920), 0x22de694e7c8b2a5f8d920b50d6c90aca30d149ace5ff9feebf84a136abdfa9f4) // fixed_comms[26].x_lo + mstore(add(payload, 0x3940), 0x00000000000000000000000000000000162f68d07437f5dd432392f6a244044d) // fixed_comms[26].y_hi + mstore(add(payload, 0x3960), 0x4605b4457384fb65577df046c667a744f851fbdb0e8751aaeeb334bf32fbd3e0) // fixed_comms[26].y_lo + // Permutation commitment 0, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3980), 0x0000000000000000000000000000000014669fe860d72f984d085b33bfffa399) // permutation_comms[0].x_hi + mstore(add(payload, 0x39a0), 0x12a9b0d0acb1ff8fd119eee9d0870eab9324293b88df67b9e1ebd180db262c72) // permutation_comms[0].x_lo + mstore(add(payload, 0x39c0), 0x000000000000000000000000000000000f3292f0f7b5707fe0874c56fdae2e88) // permutation_comms[0].y_hi + mstore(add(payload, 0x39e0), 0x2a2ba44cc1a14f736d255905838f8438fb8dbfd65ad84bcbca748a749c58955e) // permutation_comms[0].y_lo + // Permutation commitment 1, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3a00), 0x000000000000000000000000000000000ee7e9d684b203f28dd5424f3a511695) // permutation_comms[1].x_hi + mstore(add(payload, 0x3a20), 0xff88a9d696976adca8c54262e0f553756ec45b8e103453f8c18a07c3abcef5fd) // permutation_comms[1].x_lo + mstore(add(payload, 0x3a40), 0x00000000000000000000000000000000177029152e262c258c987bfbeeee14c8) // permutation_comms[1].y_hi + mstore(add(payload, 0x3a60), 0xe945c114efada4abdeff8610bea0cf199ef3f375cf15f6b437de67f3379c5801) // permutation_comms[1].y_lo + // Permutation commitment 2, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3a80), 0x000000000000000000000000000000000e2ead1ec1acc411940e7bea5d0f2114) // permutation_comms[2].x_hi + mstore(add(payload, 0x3aa0), 0x6b39327e36ebab84013dd8f5c2b3f8b576a125db88ff28db45a79a28df414b66) // permutation_comms[2].x_lo + mstore(add(payload, 0x3ac0), 0x00000000000000000000000000000000199b4ea033c01e1d83627c054ba6f2dc) // permutation_comms[2].y_hi + mstore(add(payload, 0x3ae0), 0x2a61b1277d07469ad65c413a1970c9cf179bedfc4cf2f61c1ee8ca5e4a01ab90) // permutation_comms[2].y_lo + // Permutation commitment 3, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3b00), 0x0000000000000000000000000000000011af8f4e99c419588e05414f664ae8f6) // permutation_comms[3].x_hi + mstore(add(payload, 0x3b20), 0xdded731799d69d8f5a72c124a5c6df33153e15767000ef54648a8dd23687140a) // permutation_comms[3].x_lo + mstore(add(payload, 0x3b40), 0x0000000000000000000000000000000017ba99d9323e45fe54ec6ac4daaad8d3) // permutation_comms[3].y_hi + mstore(add(payload, 0x3b60), 0x0c0dbc53d3f5f54c05667eb0e4f28da8973657bf62915fcdfc551698f56aa574) // permutation_comms[3].y_lo + // Permutation commitment 4, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3b80), 0x000000000000000000000000000000000ed68fa806cd3171cb061afebf65799d) // permutation_comms[4].x_hi + mstore(add(payload, 0x3ba0), 0xf9014492a08de2420f1234158d2f1d214b5354b7ad8cfc753d2712e6bb9b20a5) // permutation_comms[4].x_lo + mstore(add(payload, 0x3bc0), 0x0000000000000000000000000000000013f05c6e0393baeec023451082007353) // permutation_comms[4].y_hi + mstore(add(payload, 0x3be0), 0x55211ac5dd3c2267dcf706d18263fa7b7916e44ab1d5309430542f75717187c4) // permutation_comms[4].y_lo + // Permutation commitment 5, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3c00), 0x000000000000000000000000000000000a925e4ad4c2d71f096de0e53baea720) // permutation_comms[5].x_hi + mstore(add(payload, 0x3c20), 0x7a90226e678437d20f2ec9ae630075a12ae88a186bcfba1bc0444e5bc40245e7) // permutation_comms[5].x_lo + mstore(add(payload, 0x3c40), 0x0000000000000000000000000000000006ff1ce2b5e6c2c3cedd7a8465c5ed3a) // permutation_comms[5].y_hi + mstore(add(payload, 0x3c60), 0x72e4496eead09ba2cd0ef56fdd94942ed738b42b71caea3d1bd78b0ae3ec3f96) // permutation_comms[5].y_lo + // Permutation commitment 6, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3c80), 0x000000000000000000000000000000000190af4fe845524352a9f624463ae7ca) // permutation_comms[6].x_hi + mstore(add(payload, 0x3ca0), 0x406fa14f4319031fe5a7630ab2bd66c107d6cb05fa65b98ee0c5f0234c29d189) // permutation_comms[6].x_lo + mstore(add(payload, 0x3cc0), 0x0000000000000000000000000000000013036a5f7dca90b530955bf735da2920) // permutation_comms[6].y_hi + mstore(add(payload, 0x3ce0), 0x29b8759dc69e53d6c830a3bee081165d4d59442dacb50c2a06aded3193e88387) // permutation_comms[6].y_lo + // Permutation commitment 7, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3d00), 0x0000000000000000000000000000000012b6e22b800c1a2dfde554f3312f4b50) // permutation_comms[7].x_hi + mstore(add(payload, 0x3d20), 0x1b75400de7c8c476dcc9fd72dcef80d9384da4e4af8f928a07e4a9fa485da8bc) // permutation_comms[7].x_lo + mstore(add(payload, 0x3d40), 0x00000000000000000000000000000000053da19ebda674bbc95d3d10ad04cb96) // permutation_comms[7].y_hi + mstore(add(payload, 0x3d60), 0xa410aecead2c7059df1e12a1a56117a830f0008a91551cec88f1ad548a379dfe) // permutation_comms[7].y_lo + // Permutation commitment 8, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3d80), 0x0000000000000000000000000000000016038223f354e7d7c75614116c58b1c9) // permutation_comms[8].x_hi + mstore(add(payload, 0x3da0), 0x866a8a39d6b30ea560b263c68da7e78210b2f2a7f9b19564388f2f2370d7cd6a) // permutation_comms[8].x_lo + mstore(add(payload, 0x3dc0), 0x000000000000000000000000000000000bdb1d40f473070bfa087ff87bc9c86d) // permutation_comms[8].y_hi + mstore(add(payload, 0x3de0), 0x0d57398e71f45845ebcd0aa24a9ea829718dfce700af80fab46b51ad73fb58f6) // permutation_comms[8].y_lo + // Permutation commitment 9, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3e00), 0x00000000000000000000000000000000085c579504429306bbf0ec28b07f91a6) // permutation_comms[9].x_hi + mstore(add(payload, 0x3e20), 0x7326fabf9ec866a45ef4d294f390a7c497c7b990a62b602f5999fcaee0e7e9bc) // permutation_comms[9].x_lo + mstore(add(payload, 0x3e40), 0x000000000000000000000000000000000b042dab28398e8c0670ee6f04cca93a) // permutation_comms[9].y_hi + mstore(add(payload, 0x3e60), 0xa41ecc7205c8216513d027b4c1213d0e452c16898d7b2c811027598cf2715dc3) // permutation_comms[9].y_lo + // Permutation commitment 10, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3e80), 0x00000000000000000000000000000000093b6ec41dd5c2455b9d2dca635c472d) // permutation_comms[10].x_hi + mstore(add(payload, 0x3ea0), 0x386496c219c45db9f1a5d96c179d1028694626c4a1845aef384efaaa8053284b) // permutation_comms[10].x_lo + mstore(add(payload, 0x3ec0), 0x000000000000000000000000000000000386d1128886a4a879e8a93aa84b6525) // permutation_comms[10].y_hi + mstore(add(payload, 0x3ee0), 0x1061026f4dae08952816961f9d6fc0b8153784a5961f831b68f1e2ddc567efb7) // permutation_comms[10].y_lo + // Permutation commitment 11, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3f00), 0x0000000000000000000000000000000000b6ed6814a099c4e435e42fc8ee4250) // permutation_comms[11].x_hi + mstore(add(payload, 0x3f20), 0x0e16b88c78069cebe20e4d647c8bb44acd6b9c6712d53a3ebd014e54a1a75e89) // permutation_comms[11].x_lo + mstore(add(payload, 0x3f40), 0x0000000000000000000000000000000001ae3805b59b2f2d53a59e2c19001e97) // permutation_comms[11].y_hi + mstore(add(payload, 0x3f60), 0xaace57ac2ba8b58550a740e897ba6278e409291e5c87466a01edd7cbe3b99f88) // permutation_comms[11].y_lo + // Permutation commitment 12, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3f80), 0x000000000000000000000000000000000ca3af5a65ec51141f694db29736dd57) // permutation_comms[12].x_hi + mstore(add(payload, 0x3fa0), 0xa1cdbfa79f45aa80a6cdda8de3d51c4b16c95361b412c6f47e132eae7b69f7f7) // permutation_comms[12].x_lo + mstore(add(payload, 0x3fc0), 0x0000000000000000000000000000000018dc16284e57fccd755058b259b913c7) // permutation_comms[12].y_hi + mstore(add(payload, 0x3fe0), 0x7a54da0d0e206b7906878fd3e994ad04c66f48e19cd982d5651f99b48b992064) // permutation_comms[12].y_lo + // Permutation commitment 13, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x4000), 0x00000000000000000000000000000000197560c82bf47486bee750469d626ce4) // permutation_comms[13].x_hi + mstore(add(payload, 0x4020), 0x2d40ce7489ff61a64a0eb55e6672926a40f06348ab575f926fbe4d87b672fcd7) // permutation_comms[13].x_lo + mstore(add(payload, 0x4040), 0x0000000000000000000000000000000007b4f39a579a01d4f01f4ab9bc841a7d) // permutation_comms[13].y_hi + mstore(add(payload, 0x4060), 0xe70541d6b79945e9569fd59388739f5b4ce57794044d287d7b0b58e35198caab) // permutation_comms[13].y_lo + // Permutation commitment 14, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x4080), 0x0000000000000000000000000000000004e1b539a3a123291eaa8c85be3395e7) // permutation_comms[14].x_hi + mstore(add(payload, 0x40a0), 0x9c6845178d995313daf42680692210e1ec79278ec8b6e2da1d6f289609100404) // permutation_comms[14].x_lo + mstore(add(payload, 0x40c0), 0x0000000000000000000000000000000010d56b62afe35890b4f98b946daa8936) // permutation_comms[14].y_hi + mstore(add(payload, 0x40e0), 0xd83e014595585e2892e21bfc5e86f84d404b435dc1a689539d74773aba55dad4) // permutation_comms[14].y_lo + // Permutation commitment 15, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x4100), 0x000000000000000000000000000000000f227bcaef796cabf0a93289fb8321e9) // permutation_comms[15].x_hi + mstore(add(payload, 0x4120), 0x1d43880fa5fcebab73c77c6bb406cc5e9386a81774635b5a6144b75ef10cd994) // permutation_comms[15].x_lo + mstore(add(payload, 0x4140), 0x000000000000000000000000000000000a849d95d37e501cc6272f325f88d677) // permutation_comms[15].y_hi + mstore(add(payload, 0x4160), 0xb25be83ce57eb9c32d382613930bbc0ef7ec349d44ac74a9383c111efcb73783) // permutation_comms[15].y_lo + // Permutation commitment 16, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x4180), 0x000000000000000000000000000000000b77ada25b279742ec75d2014583ad79) // permutation_comms[16].x_hi + mstore(add(payload, 0x41a0), 0x427bfa99ab8a941648e1a11084fec2a6fdb8192f498cf3e272a5e786597273a1) // permutation_comms[16].x_lo + mstore(add(payload, 0x41c0), 0x00000000000000000000000000000000056788c108fd40583888b8771a70d3f6) // permutation_comms[16].y_hi + mstore(add(payload, 0x41e0), 0x364290f978d1fc201c0a76561a4c1cdde7643f7d76448dad2586c3eac2e34104) // permutation_comms[16].y_lo + // Permutation commitment 17, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x4200), 0x0000000000000000000000000000000002ecf71916494d46153fdd513ead1917) // permutation_comms[17].x_hi + mstore(add(payload, 0x4220), 0x06401b48839713727c920f4591fe09ac3e8f4140ef1cd9e39b0d0bbe0b5b87d4) // permutation_comms[17].x_lo + mstore(add(payload, 0x4240), 0x000000000000000000000000000000000e7bab8cc3c714f06826c8f3695dc63c) // permutation_comms[17].y_hi + mstore(add(payload, 0x4260), 0xe7e931ff0dbe009cbb84fef0b99124905d90cdd6f5356a7ee7673548f6f0692c) // permutation_comms[17].y_lo + + // Return exactly the INVALID prefix plus the generated payload. The + // linked verifier pins this byte length and the resulting codehash. + return(runtime, 0x4281) + } + } +} \ No newline at end of file diff --git a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md new file mode 100644 index 000000000..25ba416ac --- /dev/null +++ b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md @@ -0,0 +1,93 @@ +# Moonlight Wrap Sepolia Deployment + +This directory contains the generated Moonlight wrap verifier contracts that +were deployed to Sepolia, plus the exact runtime bytecode fetched back from the +chain. + +## Addresses + +| Contract | Address | +| --- | --- | +| `Halo2VerifyingKey` | `0x2132E1C5f2aFE710E0F3fE01ec70E95000F99882` | +| `Halo2Verifier` | `0x3d7f4A7BF9EB60EC7909880A68eaf41e4fB4B3b5` | + +## Transactions + +| Action | Transaction | +| --- | --- | +| Deploy VK | `0x5283e8cfd54be98b752cfee8fda8c0f02b196f8b0053af33850e4b2cd20df80a` | +| Deploy verifier | `0x69c4f9d0ac9098357346b3986c3fd48d062738124df91a73a89e314ecf044e0a` | +| Verify fresh proof | `0x91bac90773f107016103be132b543af5ea4457527e3866484e62cbb6e01fdb97` | + +## Runtime Metadata + +| Contract | Runtime bytes | Runtime code hash | +| --- | ---: | --- | +| `Halo2VerifyingKey` | `17025` | `0x24430c63ec2017f12ec4004d66c66314f418dd77fe38dd4c2475c8f2b259e009` | +| `Halo2Verifier` | `21368` | `0x21eafc97654cfecd897cd8957217254ce0570813e59a36bc835e8bc459ee9b55` | + +## Files + +- `Halo2VerifyingKey.sol`: generated VK source. +- `Halo2Verifier.sol`: generated verifier source. +- `Halo2VerifyingKey.runtime.bytecode.hex`: exact deployed VK runtime bytecode from Sepolia. +- `Halo2Verifier.runtime.bytecode.hex`: exact deployed verifier runtime bytecode from Sepolia. +- `deployment.json`: address, transaction, gas, and bytecode metadata. + +## Recreate The Deployment + +From `proofs/solidity-verifier`: + +```bash +scripts/install_pinned_solc.sh + +cast wallet new ~/.foundry/keystores sepolia-moonlight +cast wallet address --account sepolia-moonlight +``` + +Fund that address with native Sepolia ETH, then deploy. Foundry can prompt for +the keystore password interactively: + +```bash +SEPOLIA_RPC_URL=https://... \ +scripts/deploy_moonlight_sepolia.sh \ + --account sepolia-moonlight \ + --skip-verify +``` + +For non-interactive runs, put the keystore password in a local file outside the +repo, `chmod 600` it, and add `--password-file /path/to/password-file`. + +The deploy script writes: + +```text +target/moonlight-wrap-sepolia-deployment.env +target/moonlight-wrap-sepolia-deployment.json +``` + +## Generate And Verify A Fresh Proof + +Generate fresh Moonlight calldata, run a free `eth_call`, then broadcast a +verification transaction: + +```bash +SEPOLIA_RPC_URL=https://... \ +scripts/prove_and_verify_moonlight_sepolia.sh \ + --moonlight-dir /path/to/Moonlight \ + --deployment-env target/moonlight-wrap-sepolia-deployment.env \ + --account sepolia-moonlight \ + --no-trace \ + --send +``` + +Add `--password-file /path/to/password-file` for non-interactive keystore use. +Without `--send`, the script only runs `eth_call` and does not spend gas. + +## Requirements + +- Foundry: `forge` and `cast`. +- Pinned `solc` installed at `.solc/solc` or supplied via `SOLC`. +- Sepolia RPC URL. +- A funded Sepolia account. +- A Moonlight checkout with the wrap Solidity dump hook enabled. +- A Sepolia fork that supports EIP-2537 BLS12-381 precompiles. diff --git a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/deployment.json b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/deployment.json new file mode 100644 index 000000000..7f0b5cbbd --- /dev/null +++ b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/deployment.json @@ -0,0 +1,36 @@ +{ + "chain": "sepolia", + "chainId": 11155111, + "deployer": "0x609A4Ff8347Bb82d65a12A597298F256Fd4DE493", + "contracts": { + "Halo2VerifyingKey": { + "address": "0x2132E1C5f2aFE710E0F3fE01ec70E95000F99882", + "deploymentTx": "0x5283e8cfd54be98b752cfee8fda8c0f02b196f8b0053af33850e4b2cd20df80a", + "gasUsed": 3723458, + "runtimeBytes": 17025, + "runtimeCodeHash": "0x24430c63ec2017f12ec4004d66c66314f418dd77fe38dd4c2475c8f2b259e009", + "runtimeBytecodeFile": "Halo2VerifyingKey.runtime.bytecode.hex", + "sourceFile": "Halo2VerifyingKey.sol" + }, + "Halo2Verifier": { + "address": "0x3d7f4A7BF9EB60EC7909880A68eaf41e4fB4B3b5", + "deploymentTx": "0x69c4f9d0ac9098357346b3986c3fd48d062738124df91a73a89e314ecf044e0a", + "gasUsed": 4763839, + "runtimeBytes": 21368, + "runtimeCodeHash": "0x21eafc97654cfecd897cd8957217254ce0570813e59a36bc835e8bc459ee9b55", + "runtimeBytecodeFile": "Halo2Verifier.runtime.bytecode.hex", + "sourceFile": "Halo2Verifier.sol" + } + }, + "proofVerificationTx": { + "transactionHash": "0x91bac90773f107016103be132b543af5ea4457527e3866484e62cbb6e01fdb97", + "gasUsed": 1310889, + "status": 1 + }, + "compiler": { + "solc": "0.8.30", + "viaIr": true, + "optimizerRuns": 200, + "metadata": "none" + } +} diff --git a/proofs/solidity-verifier/fuzz/.gitignore b/proofs/solidity-verifier/fuzz/.gitignore new file mode 100644 index 000000000..f83457aec --- /dev/null +++ b/proofs/solidity-verifier/fuzz/.gitignore @@ -0,0 +1,4 @@ +artifacts/ +corpus/ +coverage/ +target/ diff --git a/proofs/solidity-verifier/fuzz/Cargo.toml b/proofs/solidity-verifier/fuzz/Cargo.toml new file mode 100644 index 000000000..b6af9ae57 --- /dev/null +++ b/proofs/solidity-verifier/fuzz/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "halo2-solidity-verifier-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +halo2_solidity_verifier = { path = "..", features = ["evm"] } +libfuzzer-sys = "0.4" +midnight-curves = { path = "../../../curves" } +midnight-proofs = { path = "../..", default-features = false, features = [ + "keccak-transcript", + "committed-instances", + "circuit-params", +] } +rand_chacha = "0.3" +sha3 = "0.10.8" + +[[bin]] +name = "proof_repack" +path = "fuzz_targets/proof_repack.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "solidity_calldata" +path = "fuzz_targets/solidity_calldata.rs" +test = false +doc = false +bench = false + +[workspace] +members = ["."] + +[patch.crates-io] +midnight-proofs = { path = "../.." } +midnight-curves = { path = "../../../curves" } + +[patch."https://github.com/midnightntwrk/midnight-zk"] +midnight-proofs = { path = "../.." } +midnight-curves = { path = "../../../curves" } diff --git a/proofs/solidity-verifier/fuzz/README.md b/proofs/solidity-verifier/fuzz/README.md new file mode 100644 index 000000000..5dcb65bec --- /dev/null +++ b/proofs/solidity-verifier/fuzz/README.md @@ -0,0 +1,43 @@ +# Solidity Verifier Fuzzing + +This directory contains `cargo-fuzz` targets for coverage-guided fuzzing. + +Install a nightly Rust toolchain and the runner once: + +```sh +rustup toolchain install nightly +cargo install cargo-fuzz +``` + +Run the current repacker target from `proofs/solidity-verifier`: + +```sh +cargo +nightly fuzz run proof_repack +``` + +For a short local smoke run: + +```sh +cargo +nightly fuzz run proof_repack -- -max_total_time=60 +``` + +`proof_repack` fuzzes compressed proof bytes through `SolidityGenerator::repack_proof`. +Inputs that successfully repack are also ABI-encoded with one public instance to +check the Solidity calldata shape. + +To fuzz the generated Solidity verifier through `revm`, run: + +```sh +cargo +nightly fuzz run solidity_calldata -- -max_total_time=60 +``` + +`solidity_calldata` builds a tiny valid proof, compiles the generated embedded +Solidity verifier once during target initialization, deploys it once per fuzzing +thread, then mutates one calldata field at a time: selector, dynamic ABI heads, +proof length, proof G1s, proof eval scalars, q evals, instance length, +instances, trailing bytes, truncation, and proof-offset drift. Any mutated input +that makes `verifyProof(bytes,uint256[])` return `true` is a fuzzer failure. + +This target needs the configured `solc` to match the pinned verifier compiler +(`0.8.30+commit.73712a01`). For local experiments with another compiler, set +`HALO2_SOLIDITY_ALLOW_UNPINNED_SOLC=1`. diff --git a/proofs/solidity-verifier/fuzz/fuzz_targets/proof_repack.rs b/proofs/solidity-verifier/fuzz/fuzz_targets/proof_repack.rs new file mode 100644 index 000000000..49c7d08a8 --- /dev/null +++ b/proofs/solidity-verifier/fuzz/fuzz_targets/proof_repack.rs @@ -0,0 +1,119 @@ +#![no_main] + +use std::sync::LazyLock; + +use halo2_solidity_verifier::{ + encode_calldata, GeneratorConfig, SolidityGenerator, FN_SIG_VERIFY_PROOF, +}; +use libfuzzer_sys::fuzz_target; +use midnight_curves::{Bls12, Fq}; +use midnight_proofs::{ + circuit::{Layouter, SimpleFloorPlanner, Value}, + plonk::{ + keygen_vk_with_k, Advice, Circuit, Column, ConstraintSystem, Constraints, Error, Selector, + }, + poly::{kzg::params::ParamsKZG, kzg::KZGCommitmentScheme, Rotation}, +}; +use rand_chacha::{rand_core::SeedableRng, ChaCha8Rng}; + +const K: u32 = 5; + +struct FuzzFixture { + generator: SolidityGenerator<'static>, +} + +static FIXTURE: LazyLock = LazyLock::new(|| { + let mut rng = ChaCha8Rng::seed_from_u64(0xf00d_f077); + let params = Box::leak(Box::new(ParamsKZG::::unsafe_setup(K, &mut rng))); + let circuit = MiniCircuit::default(); + let vk = Box::leak(Box::new( + keygen_vk_with_k::, _>(params, &circuit, K) + .expect("mini circuit VK generation"), + )); + FuzzFixture { + generator: SolidityGenerator::new(params, vk, GeneratorConfig::new(1, 1)), + } +}); + +#[derive(Clone, Debug, Default)] +struct MiniCircuit; + +#[derive(Clone, Debug)] +struct MiniConfig { + a: Column, + b: Column, + out: Column, + selector: Selector, +} + +impl Circuit for MiniCircuit { + type Config = MiniConfig; + type FloorPlanner = SimpleFloorPlanner; + type Params = (); + + fn without_witnesses(&self) -> Self { + Self + } + + fn params(&self) -> Self::Params {} + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + let a = meta.advice_column(); + let b = meta.advice_column(); + let out = meta.advice_column(); + let committed_instance = meta.instance_column(); + let public_instance = meta.instance_column(); + let selector = meta.selector(); + + meta.create_gate("mini public balance", |meta| { + let a = meta.query_advice(a, Rotation::cur()); + let b = meta.query_advice(b, Rotation::cur()); + let out = meta.query_advice(out, Rotation::cur()); + let committed = meta.query_instance(committed_instance, Rotation::cur()); + let public = meta.query_instance(public_instance, Rotation::cur()); + + Constraints::with_selector( + selector, + vec![("public balance", a + b + committed + public - out)], + ) + }); + + MiniConfig { + a, + b, + out, + selector, + } + } + + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + layouter.assign_region( + || "mini witness", + |mut region| { + config.selector.enable(&mut region, 0)?; + region.assign_advice(|| "a", config.a, 0, || Value::known(Fq::from(3)))?; + region.assign_advice(|| "b", config.b, 0, || Value::known(Fq::from(4)))?; + region.assign_advice(|| "out", config.out, 0, || Value::known(Fq::from(7)))?; + Ok(()) + }, + ) + } +} + +fuzz_target!(|compressed_proof: &[u8]| { + if let Ok(proof) = FIXTURE.generator.repack_proof(compressed_proof) { + assert_eq!(proof.len() % 32, 0, "repacked proof must be word-aligned"); + + let calldata = encode_calldata(&proof, &[Fq::from(0)]); + assert_eq!(&calldata[..4], FN_SIG_VERIFY_PROOF.as_slice()); + assert_eq!( + calldata.len(), + 4 + 0x40 + 0x20 + proof.len() + 0x20 + 0x20, + "single-instance calldata length should match ABI dynamic layout", + ); + } +}); diff --git a/proofs/solidity-verifier/fuzz/fuzz_targets/solidity_calldata.rs b/proofs/solidity-verifier/fuzz/fuzz_targets/solidity_calldata.rs new file mode 100644 index 000000000..616943c14 --- /dev/null +++ b/proofs/solidity-verifier/fuzz/fuzz_targets/solidity_calldata.rs @@ -0,0 +1,429 @@ +#![no_main] + +use std::{cell::RefCell, ops::Range, sync::LazyLock}; + +use halo2_solidity_verifier::{ + compile_solidity, encode_calldata, CallOutcome, Evm, GeneratorConfig, RenderOptions, + RepackError, SolidityGenerator, FN_SIG_VERIFY_PROOF, +}; +use libfuzzer_sys::fuzz_target; +use midnight_curves::{Bls12, Fq}; +use midnight_proofs::{ + circuit::{Layouter, SimpleFloorPlanner, Value}, + plonk::{ + create_proof, keygen_pk, keygen_vk_with_k, Advice, Circuit, Column, ConstraintSystem, + Constraints, Error, Selector, + }, + poly::{kzg::params::ParamsKZG, kzg::KZGCommitmentScheme, Rotation}, + transcript::{CircuitTranscript, Transcript}, +}; +use rand_chacha::{rand_core::SeedableRng, ChaCha8Rng}; + +const K: u32 = 5; +const SELECTOR_BYTES: usize = 4; +const WORD_BYTES: usize = 32; +const G1_BYTES: usize = 128; +const PROOF_HEAD_CPTR: usize = SELECTOR_BYTES; +const INSTANCES_HEAD_CPTR: usize = SELECTOR_BYTES + WORD_BYTES; +const PROOF_LEN_CPTR: usize = SELECTOR_BYTES + 2 * WORD_BYTES; +const PROOF_CPTR: usize = SELECTOR_BYTES + 3 * WORD_BYTES; +const MAX_RAW_CALLDATA: usize = 8192; + +struct FuzzFixture { + verifier_creation_code: Vec, + valid_calldata: Vec, + proof_len: usize, + num_instance_cptr: usize, + proof_g1_ranges: Vec>, + eval_ranges: Vec>, + q_eval_ranges: Vec>, + instance_ranges: Vec>, +} + +static FIXTURE: LazyLock = LazyLock::new(build_fixture); + +thread_local! { + static DEPLOYED: RefCell = RefCell::new(DeployedVerifier::deploy(&FIXTURE)); +} + +struct DeployedVerifier { + evm: Evm, + verifier_address: halo2_solidity_verifier::revm::primitives::Address, +} + +impl DeployedVerifier { + fn deploy(fixture: &FuzzFixture) -> Self { + let mut evm = Evm::default(); + let verifier_address = evm.create(fixture.verifier_creation_code.clone()); + let mut deployed = Self { + evm, + verifier_address, + }; + let output = deployed.call(fixture.valid_calldata.clone()); + assert!( + output_is_true(output.as_deref()), + "generated Solidity verifier must accept the seed proof before fuzzing" + ); + deployed + } + + fn call(&mut self, calldata: Vec) -> Option> { + match self.evm.try_call_with_gas(self.verifier_address, calldata, 500_000_000) { + CallOutcome::Success { output, .. } => Some(output), + CallOutcome::Revert { .. } | CallOutcome::Halt { .. } => None, + } + } +} + +#[derive(Clone, Debug, Default)] +struct MiniCircuit; + +#[derive(Clone, Debug)] +struct MiniConfig { + a: Column, + b: Column, + out: Column, + selector: Selector, +} + +impl Circuit for MiniCircuit { + type Config = MiniConfig; + type FloorPlanner = SimpleFloorPlanner; + type Params = (); + + fn without_witnesses(&self) -> Self { + Self + } + + fn params(&self) -> Self::Params {} + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + let a = meta.advice_column(); + let b = meta.advice_column(); + let out = meta.advice_column(); + let committed_instance = meta.instance_column(); + let public_instance = meta.instance_column(); + let selector = meta.selector(); + + meta.create_gate("mini public balance", |meta| { + let a = meta.query_advice(a, Rotation::cur()); + let b = meta.query_advice(b, Rotation::cur()); + let out = meta.query_advice(out, Rotation::cur()); + let committed = meta.query_instance(committed_instance, Rotation::cur()); + let public = meta.query_instance(public_instance, Rotation::cur()); + + Constraints::with_selector( + selector, + vec![("public balance", a + b + committed + public - out)], + ) + }); + + MiniConfig { + a, + b, + out, + selector, + } + } + + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + layouter.assign_region( + || "mini witness", + |mut region| { + config.selector.enable(&mut region, 0)?; + region.assign_advice(|| "a", config.a, 0, || Value::known(Fq::from(3)))?; + region.assign_advice(|| "b", config.b, 0, || Value::known(Fq::from(4)))?; + region.assign_advice(|| "out", config.out, 0, || Value::known(Fq::from(7)))?; + Ok(()) + }, + ) + } +} + +fn build_fixture() -> FuzzFixture { + let mut setup_rng = ChaCha8Rng::seed_from_u64(0x501d_f077); + let params = ParamsKZG::::unsafe_setup(K, &mut setup_rng); + let circuit = MiniCircuit; + let vk = keygen_vk_with_k::, _>(¶ms, &circuit, K) + .expect("mini circuit VK generation"); + let pk = keygen_pk(vk, &circuit).expect("mini circuit PK generation"); + + let committed = [Fq::from(0)]; + let public = [Fq::from(0)]; + let all_instance_columns: [&[Fq]; 2] = [&committed, &public]; + let mut proof_rng = ChaCha8Rng::seed_from_u64(0x51d1_f1ed); + let mut transcript = CircuitTranscript::::init(); + create_proof::, _, _>( + ¶ms, + &pk, + std::slice::from_ref(&circuit), + 1, + &[&all_instance_columns], + &mut proof_rng, + &mut transcript, + ) + .expect("mini proof generation"); + let compressed_proof = transcript.finalize(); + + let generator = SolidityGenerator::new(¶ms, pk.get_vk(), GeneratorConfig::new(1, 1)); + let layout = match generator.repack_proof(&[]) { + Err(RepackError::LengthMismatch { + expected, + prefix_g1, + num_evals, + num_point_sets, + .. + }) => { + assert_eq!( + compressed_proof.len(), + expected, + "repack length oracle must match generated proof" + ); + RepackedLayout { + prefix_g1, + num_evals, + num_point_sets, + } + } + other => panic!("expected repack length mismatch for empty proof, got {other:?}"), + }; + let proof = generator.repack_proof(&compressed_proof).expect("proof repack"); + assert_eq!( + proof.len(), + layout.repacked_len(), + "repacked proof length should match layout oracle" + ); + let valid_calldata = encode_calldata(&proof, &public); + let verifier_solidity = generator + .render(RenderOptions::default()) + .expect("embedded verifier render") + .verifier; + let verifier_creation_code = compile_solidity(verifier_solidity); + let num_instance_cptr = PROOF_CPTR + proof.len(); + let instance_cptr = num_instance_cptr + WORD_BYTES; + + let mut proof_g1_ranges = Vec::new(); + for idx in 0..layout.prefix_g1 { + push_range(&mut proof_g1_ranges, PROOF_CPTR + idx * G1_BYTES, G1_BYTES); + } + let eval_offset = PROOF_CPTR + layout.prefix_g1 * G1_BYTES; + let eval_ranges = word_ranges(eval_offset, layout.num_evals); + let f_com_cptr = eval_offset + layout.num_evals * WORD_BYTES; + push_range(&mut proof_g1_ranges, f_com_cptr, G1_BYTES); + let q_eval_cptr = f_com_cptr + G1_BYTES; + let q_eval_ranges = word_ranges(q_eval_cptr, layout.num_point_sets); + let pi_cptr = q_eval_cptr + layout.num_point_sets * WORD_BYTES; + push_range(&mut proof_g1_ranges, pi_cptr, G1_BYTES); + assert_eq!(pi_cptr + G1_BYTES, num_instance_cptr); + + FuzzFixture { + verifier_creation_code, + valid_calldata, + proof_len: proof.len(), + num_instance_cptr, + proof_g1_ranges, + eval_ranges, + q_eval_ranges, + instance_ranges: word_ranges(instance_cptr, public.len()), + } +} + +#[derive(Clone, Copy, Debug)] +struct RepackedLayout { + prefix_g1: usize, + num_evals: usize, + num_point_sets: usize, +} + +impl RepackedLayout { + fn repacked_len(self) -> usize { + self.prefix_g1 * G1_BYTES + + self.num_evals * WORD_BYTES + + G1_BYTES + + self.num_point_sets * WORD_BYTES + + G1_BYTES + } +} + +fn push_range(ranges: &mut Vec>, start: usize, len: usize) { + ranges.push(start..start + len); +} + +fn word_ranges(start: usize, len: usize) -> Vec> { + (0..len) + .map(|idx| { + let start = start + idx * WORD_BYTES; + start..start + WORD_BYTES + }) + .collect() +} + +fuzz_target!(|input: &[u8]| { + if input.is_empty() { + return; + } + + let fixture = &*FIXTURE; + let calldata = mutated_calldata(fixture, input); + if calldata == fixture.valid_calldata { + return; + } + + let output = DEPLOYED.with(|deployed| deployed.borrow_mut().call(calldata)); + assert!( + !output_is_true(output.as_deref()), + "mutated Solidity verifier calldata returned true" + ); +}); + +fn mutated_calldata(fixture: &FuzzFixture, input: &[u8]) -> Vec { + let mut calldata = fixture.valid_calldata.clone(); + match input[0] % 13 { + 0 => mutate_selector(&mut calldata, input), + 1 => mutate_abi_head(&mut calldata, PROOF_HEAD_CPTR, 0x40, input), + 2 => mutate_abi_head( + &mut calldata, + INSTANCES_HEAD_CPTR, + 0x40 + WORD_BYTES + fixture.proof_len, + input, + ), + 3 => mutate_abi_head(&mut calldata, PROOF_LEN_CPTR, fixture.proof_len, input), + 4 => mutate_one_range(&mut calldata, &fixture.proof_g1_ranges, input), + 5 => mutate_one_range(&mut calldata, &fixture.eval_ranges, input), + 6 => mutate_one_range(&mut calldata, &fixture.q_eval_ranges, input), + 7 => mutate_abi_head(&mut calldata, fixture.num_instance_cptr, 1, input), + 8 => mutate_one_range(&mut calldata, &fixture.instance_ranges, input), + 9 => append_trailing_bytes(&mut calldata, input), + 10 => truncate_calldata(&mut calldata, input), + 11 => insert_or_delete_near_proof(&mut calldata, fixture, input), + _ => replace_with_raw_calldata(&mut calldata, input), + } + calldata +} + +fn mutate_selector(calldata: &mut [u8], input: &[u8]) { + let idx = pick(input, 1, FN_SIG_VERIFY_PROOF.len()); + calldata[idx] ^= nonzero_byte(input, 2); +} + +fn mutate_abi_head(calldata: &mut [u8], offset: usize, valid: usize, input: &[u8]) { + let value = match pick(input, 1, 8) { + 0 => 0, + 1 => valid.saturating_sub(WORD_BYTES), + 2 => valid + WORD_BYTES, + 3 => valid + pick(input, 2, 9) * WORD_BYTES, + 4 => SELECTOR_BYTES, + 5 => PROOF_CPTR, + 6 => usize::MAX, + _ => return overwrite_word_from_input(calldata, offset, input, 2), + }; + write_word_usize(calldata, offset, value); + if read_word_usize(calldata, offset) == Some(valid) { + calldata[offset + WORD_BYTES - 1] ^= 1; + } +} + +fn mutate_one_range(calldata: &mut [u8], ranges: &[Range], input: &[u8]) { + if ranges.is_empty() { + mutate_selector(calldata, input); + return; + } + let range = &ranges[pick(input, 1, ranges.len())]; + let idx = range.start + pick(input, 2, range.len()); + calldata[idx] ^= nonzero_byte(input, 3); +} + +fn append_trailing_bytes(calldata: &mut Vec, input: &[u8]) { + let len = 1 + pick(input, 1, 256); + for idx in 0..len { + calldata.push(input.get(2 + idx).copied().unwrap_or(idx as u8)); + } +} + +fn truncate_calldata(calldata: &mut Vec, input: &[u8]) { + if calldata.is_empty() { + return; + } + let len = pick(input, 1, calldata.len()); + calldata.truncate(len); +} + +fn insert_or_delete_near_proof(calldata: &mut Vec, fixture: &FuzzFixture, input: &[u8]) { + let positions = [ + PROOF_LEN_CPTR, + PROOF_CPTR, + PROOF_CPTR + pick(input, 2, fixture.proof_len.max(1)), + fixture.num_instance_cptr, + ]; + let pos = positions[pick(input, 1, positions.len())].min(calldata.len()); + let len = WORD_BYTES.min(calldata.len().saturating_sub(pos)).max(1); + if pick(input, 3, 2) == 0 && calldata.len() + WORD_BYTES <= MAX_RAW_CALLDATA { + calldata.splice(pos..pos, [0u8; WORD_BYTES]); + } else { + calldata.drain(pos..pos + len); + } +} + +fn replace_with_raw_calldata(calldata: &mut Vec, input: &[u8]) { + let raw = input.get(1..).unwrap_or_default(); + let len = raw.len().min(MAX_RAW_CALLDATA); + calldata.clear(); + calldata.extend_from_slice(&raw[..len]); +} + +fn overwrite_word_from_input( + calldata: &mut [u8], + offset: usize, + input: &[u8], + input_offset: usize, +) { + if offset + WORD_BYTES > calldata.len() { + return; + } + for idx in 0..WORD_BYTES { + calldata[offset + idx] = input.get(input_offset + idx).copied().unwrap_or(idx as u8); + } +} + +fn write_word_usize(calldata: &mut [u8], offset: usize, value: usize) { + if offset + WORD_BYTES > calldata.len() { + return; + } + calldata[offset..offset + WORD_BYTES].fill(0); + let bytes = value.to_be_bytes(); + calldata[offset + WORD_BYTES - bytes.len()..offset + WORD_BYTES].copy_from_slice(&bytes); +} + +fn read_word_usize(calldata: &[u8], offset: usize) -> Option { + if offset + WORD_BYTES > calldata.len() { + return None; + } + let mut bytes = [0u8; std::mem::size_of::()]; + let source = &calldata[offset + WORD_BYTES - bytes.len()..offset + WORD_BYTES]; + bytes.copy_from_slice(source); + Some(usize::from_be_bytes(bytes)) +} + +fn output_is_true(output: Option<&[u8]>) -> bool { + let Some(output) = output else { + return false; + }; + output.len() == WORD_BYTES + && output[..WORD_BYTES - 1].iter().all(|byte| *byte == 0) + && output[WORD_BYTES - 1] == 1 +} + +fn pick(input: &[u8], offset: usize, modulo: usize) -> usize { + if modulo == 0 { + return 0; + } + usize::from(input.get(offset).copied().unwrap_or(0)) % modulo +} + +fn nonzero_byte(input: &[u8], offset: usize) -> u8 { + input.get(offset).copied().unwrap_or(1) | 1 +} diff --git a/proofs/solidity-verifier/scripts/deploy_moonlight_sepolia.sh b/proofs/solidity-verifier/scripts/deploy_moonlight_sepolia.sh new file mode 100755 index 000000000..2e4d43c29 --- /dev/null +++ b/proofs/solidity-verifier/scripts/deploy_moonlight_sepolia.sh @@ -0,0 +1,287 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +DUMP_DIR="${MOONLIGHT_WRAP_SOLIDITY_DUMP_DIR:-"$ROOT_DIR/target/moonlight-wrap-solidity-dump"}" +RPC_URL="${SEPOLIA_RPC_URL:-${ETH_RPC_URL:-}}" +CHAIN="${CHAIN:-sepolia}" +SOLC_BIN="${SOLC:-"$ROOT_DIR/.solc/solc"}" +OPTIMIZER_RUNS="${SOLC_OPTIMIZE_RUNS:-200}" +EVM_VERSION="${EVM_VERSION:-cancun}" +VERIFY_CONTRACTS=1 +VERIFIER_PROVIDER="${VERIFIER_PROVIDER:-etherscan}" +ETHERSCAN_KEY="${ETHERSCAN_API_KEY:-}" +VK_ADDRESS="${MOONLIGHT_VK_ADDRESS:-}" +OUTPUT_ENV="${MOONLIGHT_DEPLOYMENT_ENV:-"$ROOT_DIR/target/moonlight-wrap-sepolia-deployment.env"}" +OUTPUT_JSON="${MOONLIGHT_DEPLOYMENT_JSON:-"$ROOT_DIR/target/moonlight-wrap-sepolia-deployment.json"}" +DEPLOY_GAS_LIMIT="${DEPLOY_GAS_LIMIT:-}" +PRIVATE_KEY_VALUE="${PRIVATE_KEY:-${ETH_PRIVATE_KEY:-}}" +WALLET_KEYSTORE="${ETH_KEYSTORE:-${MOONLIGHT_SEPOLIA_KEYSTORE:-}}" +WALLET_ACCOUNT="${ETH_KEYSTORE_ACCOUNT:-${MOONLIGHT_SEPOLIA_ACCOUNT:-}}" +WALLET_PASSWORD_FILE="${ETH_PASSWORD_FILE:-${MOONLIGHT_SEPOLIA_PASSWORD_FILE:-}}" +WALLET_FROM="${ETH_FROM:-${MOONLIGHT_SEPOLIA_FROM:-}}" +WALLET_ARGS=() + +if [[ -z "$WALLET_PASSWORD_FILE" && -n "${ETH_PASSWORD:-}" && -f "${ETH_PASSWORD:-}" ]]; then + WALLET_PASSWORD_FILE="$ETH_PASSWORD" +fi + +usage() { + cat <<'USAGE' +Deploy generated Moonlight wrap verifier contracts on Sepolia. + +Usage: + scripts/deploy_moonlight_sepolia.sh [options] + +Required: + --rpc-url URL Sepolia RPC URL, or set SEPOLIA_RPC_URL / ETH_RPC_URL. + A Foundry wallet option, or set PRIVATE_KEY / ETH_PRIVATE_KEY. + The script also honors ETH_KEYSTORE_ACCOUNT / + MOONLIGHT_SEPOLIA_ACCOUNT with a password file. + +Options: + --dump-dir DIR Directory containing Halo2Verifier.sol and + Halo2VerifyingKey.sol. Defaults to + target/moonlight-wrap-solidity-dump. + --vk-address ADDRESS Reuse an existing Halo2VerifyingKey and deploy only + Halo2Verifier against it. + --output-env PATH Write addresses as shell env. Defaults to + target/moonlight-wrap-sepolia-deployment.env. + --output-json PATH Write addresses as JSON. Defaults to + target/moonlight-wrap-sepolia-deployment.json. + --solc PATH solc binary. Defaults to $SOLC or .solc/solc. + --optimizer-runs N Solidity optimizer runs. Defaults to 200. + --evm-version NAME Solidity EVM target. Defaults to cancun. + --chain NAME_OR_ID Foundry chain. Defaults to sepolia. + --gas-limit GAS Optional deployment gas limit. + --skip-verify Do not submit source verification. + --verifier NAME Foundry verifier provider. Defaults to etherscan. + --etherscan-api-key KEY Etherscan API key, or set ETHERSCAN_API_KEY. + +Wallet options forwarded to forge: + --private-key KEY | --interactive | --ledger | --trezor | --browser + --keystore PATH --account NAME --password-file PATH --from ADDRESS + +Examples: + SEPOLIA_RPC_URL=https://... PRIVATE_KEY=0x... ETHERSCAN_API_KEY=... \ + scripts/deploy_moonlight_sepolia.sh + + source target/moonlight-wrap-sepolia-wallet.env + SEPOLIA_RPC_URL=https://... scripts/deploy_moonlight_sepolia.sh --skip-verify + + scripts/deploy_moonlight_sepolia.sh --vk-address 0x... --skip-verify +USAGE +} + +die() { + echo "[moonlight-deploy] error: $*" >&2 + exit 1 +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || die "required command not found on PATH: $1" +} + +is_address() { + [[ "${1:-}" =~ ^0x[0-9a-fA-F]{40}$ ]] +} + +while (($#)); do + case "$1" in + --dump-dir) + [[ $# -ge 2 ]] || die "--dump-dir requires DIR" + DUMP_DIR="$2" + shift + ;; + --rpc-url) + [[ $# -ge 2 ]] || die "--rpc-url requires URL" + RPC_URL="$2" + shift + ;; + --vk-address) + [[ $# -ge 2 ]] || die "--vk-address requires ADDRESS" + VK_ADDRESS="$2" + shift + ;; + --output-env) + [[ $# -ge 2 ]] || die "--output-env requires PATH" + OUTPUT_ENV="$2" + shift + ;; + --output-json) + [[ $# -ge 2 ]] || die "--output-json requires PATH" + OUTPUT_JSON="$2" + shift + ;; + --solc) + [[ $# -ge 2 ]] || die "--solc requires PATH" + SOLC_BIN="$2" + shift + ;; + --optimizer-runs) + [[ $# -ge 2 ]] || die "--optimizer-runs requires N" + OPTIMIZER_RUNS="$2" + shift + ;; + --evm-version) + [[ $# -ge 2 ]] || die "--evm-version requires NAME" + EVM_VERSION="$2" + shift + ;; + --chain) + [[ $# -ge 2 ]] || die "--chain requires NAME_OR_ID" + CHAIN="$2" + shift + ;; + --gas-limit) + [[ $# -ge 2 ]] || die "--gas-limit requires GAS" + DEPLOY_GAS_LIMIT="$2" + shift + ;; + --skip-verify) + VERIFY_CONTRACTS=0 + ;; + --verifier) + [[ $# -ge 2 ]] || die "--verifier requires NAME" + VERIFIER_PROVIDER="$2" + shift + ;; + --etherscan-api-key) + [[ $# -ge 2 ]] || die "--etherscan-api-key requires KEY" + ETHERSCAN_KEY="$2" + shift + ;; + --private-key) + [[ $# -ge 2 ]] || die "--private-key requires KEY" + WALLET_ARGS+=(--private-key "$2") + shift + ;; + --interactive|--ledger|--trezor|--browser) + WALLET_ARGS+=("$1") + ;; + --keystore|--account|--password-file|--from) + [[ $# -ge 2 ]] || die "$1 requires a value" + WALLET_ARGS+=("$1" "$2") + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "unknown option: $1" + ;; + esac + shift +done + +[[ -n "$RPC_URL" ]] || die "missing Sepolia RPC URL; pass --rpc-url or set SEPOLIA_RPC_URL" +[[ -d "$DUMP_DIR" ]] || die "dump directory not found: $DUMP_DIR" +[[ -f "$DUMP_DIR/Halo2Verifier.sol" ]] || die "missing $DUMP_DIR/Halo2Verifier.sol" +[[ -f "$DUMP_DIR/Halo2VerifyingKey.sol" ]] || die "missing $DUMP_DIR/Halo2VerifyingKey.sol" +[[ -x "$SOLC_BIN" || "$(command -v "$SOLC_BIN" 2>/dev/null || true)" ]] || die "solc not found: $SOLC_BIN" +if [[ -n "$VK_ADDRESS" ]]; then + is_address "$VK_ADDRESS" || die "--vk-address is not an Ethereum address: $VK_ADDRESS" +fi +if [[ ${#WALLET_ARGS[@]} -eq 0 && -n "$PRIVATE_KEY_VALUE" ]]; then + WALLET_ARGS+=(--private-key "$PRIVATE_KEY_VALUE") +fi +if [[ ${#WALLET_ARGS[@]} -eq 0 ]]; then + if [[ -n "$WALLET_KEYSTORE" ]]; then + WALLET_ARGS+=(--keystore "$WALLET_KEYSTORE") + fi + if [[ -n "$WALLET_ACCOUNT" ]]; then + WALLET_ARGS+=(--account "$WALLET_ACCOUNT") + fi + if [[ -n "$WALLET_PASSWORD_FILE" ]]; then + [[ -f "$WALLET_PASSWORD_FILE" ]] || die "wallet password file not found: $WALLET_PASSWORD_FILE" + WALLET_ARGS+=(--password-file "$WALLET_PASSWORD_FILE") + fi + if [[ -n "$WALLET_FROM" ]]; then + WALLET_ARGS+=(--from "$WALLET_FROM") + fi +fi +if [[ ${#WALLET_ARGS[@]} -eq 0 ]]; then + die "missing wallet; pass --private-key/--interactive/etc. or set PRIVATE_KEY" +fi + +require_cmd forge +require_cmd cast + +COMMON_CREATE_ARGS=( + --broadcast + --rpc-url "$RPC_URL" + --chain "$CHAIN" + --use "$SOLC_BIN" + --via-ir + --evm-version "$EVM_VERSION" + --optimize + --optimizer-runs "$OPTIMIZER_RUNS" + --no-metadata + --root "$ROOT_DIR" +) +if [[ -n "$DEPLOY_GAS_LIMIT" ]]; then + COMMON_CREATE_ARGS+=(--gas-limit "$DEPLOY_GAS_LIMIT") +fi +if [[ "$VERIFY_CONTRACTS" -eq 1 ]]; then + [[ -n "$ETHERSCAN_KEY" ]] || die "ETHERSCAN_API_KEY is required unless --skip-verify is set" + COMMON_CREATE_ARGS+=( + --verify + --verifier "$VERIFIER_PROVIDER" + --etherscan-api-key "$ETHERSCAN_KEY" + ) +fi + +LAST_DEPLOYED="" +run_create() { + local label="$1" + shift + local log + log="$(mktemp)" + echo "[moonlight-deploy] deploying $label" + if ! forge create "${COMMON_CREATE_ARGS[@]}" "${WALLET_ARGS[@]}" "$@" 2>&1 | tee "$log"; then + rm -f "$log" + die "forge create failed for $label" + fi + LAST_DEPLOYED="$(awk '/Deployed to:/ {print $3}' "$log" | tail -n 1)" + rm -f "$log" + is_address "$LAST_DEPLOYED" || die "could not parse deployed address for $label" + echo "[moonlight-deploy] $label = $LAST_DEPLOYED" +} + +if [[ -z "$VK_ADDRESS" ]]; then + run_create \ + "Halo2VerifyingKey" \ + "$DUMP_DIR/Halo2VerifyingKey.sol:Halo2VerifyingKey" + VK_ADDRESS="$LAST_DEPLOYED" +else + echo "[moonlight-deploy] reusing Halo2VerifyingKey = $VK_ADDRESS" +fi + +run_create \ + "Halo2Verifier" \ + "$DUMP_DIR/Halo2Verifier.sol:Halo2Verifier" \ + --constructor-args "$VK_ADDRESS" +VERIFIER_ADDRESS="$LAST_DEPLOYED" + +mkdir -p "$(dirname "$OUTPUT_ENV")" "$(dirname "$OUTPUT_JSON")" +{ + printf 'MOONLIGHT_VK_ADDRESS=%q\n' "$VK_ADDRESS" + printf 'MOONLIGHT_VERIFIER_ADDRESS=%q\n' "$VERIFIER_ADDRESS" + printf 'MOONLIGHT_DEPLOY_CHAIN=%q\n' "$CHAIN" + printf 'MOONLIGHT_DUMP_DIR=%q\n' "$DUMP_DIR" +} >"$OUTPUT_ENV" +cat >"$OUTPUT_JSON" <&2 + exit 1 +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || die "required command not found on PATH: $1" +} + +is_address() { + [[ "${1:-}" =~ ^0x[0-9a-fA-F]{40}$ ]] +} + +while (($#)); do + case "$1" in + --deploy) + RUN_DEPLOY=1 + ;; + --vk-address) + [[ $# -ge 2 ]] || die "--vk-address requires ADDRESS" + VK_ADDRESS="$2" + shift + ;; + --deployment-env) + [[ $# -ge 2 ]] || die "--deployment-env requires PATH" + DEPLOYMENT_ENV="$2" + shift + ;; + --moonlight-dir) + [[ $# -ge 2 ]] || die "--moonlight-dir requires DIR" + MOONLIGHT_DIR="$2" + shift + ;; + --dump-dir) + [[ $# -ge 2 ]] || die "--dump-dir requires DIR" + DUMP_DIR="$2" + shift + ;; + --srs-dir) + [[ $# -ge 2 ]] || die "--srs-dir requires DIR" + SRS_DIR="$2" + shift + ;; + --rpc-url) + [[ $# -ge 2 ]] || die "--rpc-url requires URL" + RPC_URL="$2" + shift + ;; + --chain) + [[ $# -ge 2 ]] || die "--chain requires NAME_OR_ID" + CHAIN="$2" + shift + ;; + --solc) + [[ $# -ge 2 ]] || die "--solc requires PATH" + SOLC_BIN="$2" + shift + ;; + --gas-limit) + [[ $# -ge 2 ]] || die "--gas-limit requires GAS" + GAS_LIMIT="$2" + shift + ;; + --verifier) + [[ $# -ge 2 ]] || die "--verifier requires ADDRESS" + VERIFIER_ADDRESS="$2" + shift + ;; + --skip-prove) + RUN_PROVE=0 + ;; + --trace) + RUN_TRACE=1 + ;; + --no-trace) + RUN_TRACE=0 + ;; + --send) + RUN_SEND=1 + ;; + --skip-contract-verify) + SKIP_CONTRACT_VERIFY=1 + ;; + --private-key) + [[ $# -ge 2 ]] || die "--private-key requires KEY" + WALLET_ARGS+=(--private-key "$2") + shift + ;; + --interactive|--ledger|--trezor|--browser) + WALLET_ARGS+=("$1") + ;; + --keystore|--account|--password-file|--from) + [[ $# -ge 2 ]] || die "$1 requires a value" + WALLET_ARGS+=("$1" "$2") + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "unknown option: $1" + ;; + esac + shift +done + +[[ -n "$RPC_URL" ]] || die "missing Sepolia RPC URL; pass --rpc-url or set SEPOLIA_RPC_URL" +if [[ -n "$VERIFIER_ADDRESS" ]]; then + is_address "$VERIFIER_ADDRESS" || die "--verifier is not an Ethereum address: $VERIFIER_ADDRESS" +fi +if [[ -n "$VK_ADDRESS" ]]; then + is_address "$VK_ADDRESS" || die "--vk-address is not an Ethereum address: $VK_ADDRESS" +fi +if [[ ${#WALLET_ARGS[@]} -eq 0 && -n "$PRIVATE_KEY_VALUE" ]]; then + WALLET_ARGS+=(--private-key "$PRIVATE_KEY_VALUE") +fi +if [[ ${#WALLET_ARGS[@]} -eq 0 ]]; then + if [[ -n "$WALLET_KEYSTORE" ]]; then + WALLET_ARGS+=(--keystore "$WALLET_KEYSTORE") + fi + if [[ -n "$WALLET_ACCOUNT" ]]; then + WALLET_ARGS+=(--account "$WALLET_ACCOUNT") + fi + if [[ -n "$WALLET_PASSWORD_FILE" ]]; then + [[ -f "$WALLET_PASSWORD_FILE" ]] || die "wallet password file not found: $WALLET_PASSWORD_FILE" + WALLET_ARGS+=(--password-file "$WALLET_PASSWORD_FILE") + fi + if [[ -n "$WALLET_FROM" ]]; then + WALLET_ARGS+=(--from "$WALLET_FROM") + fi +fi + +require_cmd cast +require_cmd xxd + +if [[ "$RUN_PROVE" -eq 1 ]]; then + require_cmd cargo + [[ -f "$MOONLIGHT_DIR/aggregation/Cargo.toml" ]] || die "Moonlight aggregation manifest not found: $MOONLIGHT_DIR/aggregation/Cargo.toml" + mkdir -p "$DUMP_DIR" + marker="$(mktemp)" + echo "[moonlight-prove-verify] generating Moonlight proof and Solidity calldata" + echo "[moonlight-prove-verify] dump dir: $DUMP_DIR" + SOLC="$SOLC_BIN" \ + SRS_DIR="$SRS_DIR" \ + MOONLIGHT_RUN_WRAP_SOLIDITY_BENCH=1 \ + MOONLIGHT_RUN_WRAP_SOLIDITY_TRACE="$RUN_TRACE" \ + MOONLIGHT_WRAP_SOLIDITY_DUMP_DIR="$DUMP_DIR" \ + cargo test --manifest-path "$MOONLIGHT_DIR/aggregation/Cargo.toml" \ + "$MOONLIGHT_TEST" --release --lib -- --ignored --nocapture + if [[ ! "$DUMP_DIR/calldata.bin" -nt "$marker" || ! "$DUMP_DIR/proof.bin" -nt "$marker" ]]; then + rm -f "$marker" + die "Moonlight run did not refresh calldata.bin/proof.bin. Check that the Moonlight checkout includes the Solidity dump hooks." + fi + rm -f "$marker" +fi + +[[ -f "$DUMP_DIR/calldata.bin" ]] || die "missing calldata: $DUMP_DIR/calldata.bin" +[[ -f "$DUMP_DIR/proof.bin" ]] || die "missing proof: $DUMP_DIR/proof.bin" +selector="$(xxd -p -l 4 "$DUMP_DIR/calldata.bin")" +[[ "$selector" == "1e8e1e13" ]] || die "calldata.bin has unexpected selector 0x$selector" + +if [[ "$RUN_DEPLOY" -eq 1 ]]; then + if [[ -n "$VERIFIER_ADDRESS" ]]; then + die "--deploy conflicts with --verifier; pass one or the other" + fi + require_cmd forge + deploy_args=( + --dump-dir "$DUMP_DIR" + --rpc-url "$RPC_URL" + --chain "$CHAIN" + --solc "$SOLC_BIN" + --output-env "$DEPLOYMENT_ENV" + ) + if [[ -n "$VK_ADDRESS" ]]; then + deploy_args+=(--vk-address "$VK_ADDRESS") + fi + if [[ "$SKIP_CONTRACT_VERIFY" -eq 1 ]]; then + deploy_args+=(--skip-verify) + fi + deploy_args+=("${WALLET_ARGS[@]}") + "$ROOT_DIR/scripts/deploy_moonlight_sepolia.sh" "${deploy_args[@]}" +fi + +if [[ -z "$VERIFIER_ADDRESS" && -f "$DEPLOYMENT_ENV" ]]; then + # shellcheck disable=SC1090 + source "$DEPLOYMENT_ENV" + VERIFIER_ADDRESS="${MOONLIGHT_VERIFIER_ADDRESS:-}" +fi +is_address "$VERIFIER_ADDRESS" || die "missing verifier address; pass --verifier, --deploy, or --deployment-env" + +CALLDATA="0x$(xxd -p -c 0 "$DUMP_DIR/calldata.bin")" +EXPECTED="0x0000000000000000000000000000000000000000000000000000000000000001" + +echo "[moonlight-prove-verify] eth_call verifyProof on $VERIFIER_ADDRESS" +OUTPUT="$(cast call "$VERIFIER_ADDRESS" --data "$CALLDATA" --rpc-url "$RPC_URL" --gas-limit "$GAS_LIMIT")" +if [[ "$OUTPUT" != "$EXPECTED" ]]; then + die "Sepolia eth_call returned $OUTPUT; expected $EXPECTED" +fi +echo "[moonlight-prove-verify] PASS: Sepolia eth_call returned true" + +if [[ "$RUN_SEND" -eq 1 ]]; then + if [[ ${#WALLET_ARGS[@]} -eq 0 ]]; then + die "--send needs a wallet option or PRIVATE_KEY" + fi + echo "[moonlight-prove-verify] broadcasting verifyProof transaction" + cast send "$VERIFIER_ADDRESS" "$CALLDATA" \ + --rpc-url "$RPC_URL" \ + --chain "$CHAIN" \ + --gas-limit "$GAS_LIMIT" \ + "${WALLET_ARGS[@]}" +fi diff --git a/proofs/solidity-verifier/src/test.rs b/proofs/solidity-verifier/src/test.rs index 2cdd3e9cd..5fb52090b 100644 --- a/proofs/solidity-verifier/src/test.rs +++ b/proofs/solidity-verifier/src/test.rs @@ -62,6 +62,27 @@ type PoseidonVerifierParams = const POSEIDON_K: u32 = 6; /// Environment flag that opts into expensive EVM/Solidity integration tests. const RUN_EVM_TESTS_ENV: &str = "HALO2_SOLIDITY_RUN_EVM_TESTS"; +/// Minimal caller used to exercise the production verifier under STATICCALL. +const STATICCALL_VERIFIER_HARNESS: &str = r#" +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.24; + +contract StaticcallVerifierHarness { + address immutable verifier; + + constructor(address verifier_) { + verifier = verifier_; + } + + function check(bytes calldata verifyCalldata) external view returns (bool) { + (bool ok, bytes memory output) = verifier.staticcall(verifyCalldata); + if (!ok || output.length != 32) { + return false; + } + return abi.decode(output, (bool)); + } +} +"#; #[test] fn function_signature() { @@ -551,6 +572,22 @@ fn supported_shape_circuit_fuzz_e2e() { } } +#[cfg(feature = "rust-verifier-trace")] +#[test] +fn pbt_transcript_differential_fuzzer_matches_solidity_trace() { + if !shape_fuzz_inputs_available_for_evm() { + return; + } + + let mut runner = new_property_test_runner(); + runner + .run(&any::(), |seed| { + run_transcript_differential_shape_fuzz_case(seed); + Ok(()) + }) + .unwrap(); +} + /// Render, deploy, and exercise one supported shape-fuzz case end to end. fn run_supported_shape_fuzz_case(case: &ShapeFuzzCase) -> bool { let circuit = ShapeFuzzCircuit::new(case.spec, case.seed); @@ -625,7 +662,7 @@ fn run_supported_shape_fuzz_case(case: &ShapeFuzzCase) -> bool { #[cfg(feature = "rust-verifier-trace")] let compared_selector_folds = assert_shape_fuzz_trace_matches_native_midfall( - case, + case.name, ¶ms, pk.get_vk(), &generator, @@ -657,6 +694,67 @@ fn run_supported_shape_fuzz_case(case: &ShapeFuzzCase) -> bool { compared_selector_folds } +/// Generate a random-ish supported circuit shape from a fuzz seed. +#[cfg(feature = "rust-verifier-trace")] +fn generated_shape_fuzz_spec(seed: u64) -> ShapeFuzzSpec { + ShapeFuzzSpec { + next_rotation: seed & 0x01 != 0, + second_phase: seed & 0x02 != 0, + permutation: seed & 0x04 != 0, + lookup: seed & 0x08 != 0, + additive_selector: seed & 0x10 != 0, + complex_selector: seed & 0x20 != 0, + fixed_scale: seed & 0x40 != 0, + tag: 100 + seed.rotate_left(17) % 10_000, + } +} + +#[cfg(feature = "rust-verifier-trace")] +fn run_transcript_differential_shape_fuzz_case(seed: u64) { + let spec = generated_shape_fuzz_spec(seed); + let context = format!("transcript differential seed={seed:#018x} spec={spec:?}"); + let circuit = ShapeFuzzCircuit::new(spec, seed); + let mut setup_rng = ChaCha8Rng::seed_from_u64(seed ^ 0x7ace_f00d); + let params = PoseidonParams::unsafe_setup(5, &mut setup_rng); + let vk = keygen_vk_with_k::, _>(¶ms, &circuit, 5) + .unwrap_or_else(|err| panic!("{context} vk generation failed: {err:?}")); + let pk = keygen_pk(vk, &circuit) + .unwrap_or_else(|err| panic!("{context} pk generation failed: {err:?}")); + + let committed = [F::ZERO]; + let public = [circuit.public_instance()]; + let all_instance_columns: [&[F]; 2] = [&committed, &public]; + let mut proof_rng = ChaCha8Rng::seed_from_u64(seed ^ 0x51d1_f1ed); + let mut transcript = CircuitTranscript::::init(); + create_proof::, _, _>( + ¶ms, + &pk, + std::slice::from_ref(&circuit), + 1, + &[&all_instance_columns], + &mut proof_rng, + &mut transcript, + ) + .unwrap_or_else(|err| panic!("{context} proof generation failed: {err:?}")); + let compressed_proof = transcript.finalize(); + + let generator = + SolidityGenerator::new(¶ms, pk.get_vk(), GeneratorConfig::new(public.len(), 1)); + let repacked_proof = generator + .repack_proof(&compressed_proof) + .unwrap_or_else(|err| panic!("{context} repack failed: {err:?}")); + + assert_shape_fuzz_trace_matches_native_midfall( + &context, + ¶ms, + pk.get_vk(), + &generator, + &compressed_proof, + &repacked_proof, + &public, + ); +} + /// Return whether expensive shape-fuzz EVM tests have their host prerequisites. fn shape_fuzz_inputs_available_for_evm() -> bool { if !env_flag_enabled(RUN_EVM_TESTS_ENV) { @@ -672,7 +770,7 @@ fn shape_fuzz_inputs_available_for_evm() -> bool { #[cfg(feature = "rust-verifier-trace")] fn assert_shape_fuzz_trace_matches_native_midfall( - case: &ShapeFuzzCase, + context: &str, params: &PoseidonParams, vk: &midnight_proofs::plonk::VerifyingKey>, generator: &SolidityGenerator<'_>, @@ -692,24 +790,13 @@ fn assert_shape_fuzz_trace_matches_native_midfall( &[&public_columns], &mut transcript, ) - .unwrap_or_else(|err| { - panic!( - "shape fuzz `{}` native trace prepare failed: {err:?}", - case.name - ) - }); - transcript.assert_empty().unwrap_or_else(|_| { - panic!( - "shape fuzz `{}` native trace transcript had trailing bytes", - case.name - ) - }); - guard.verify(¶ms.verifier_params()).unwrap_or_else(|err| { - panic!( - "shape fuzz `{}` native trace guard failed: {err:?}", - case.name - ) - }); + .unwrap_or_else(|err| panic!("{context} native trace prepare failed: {err:?}")); + transcript + .assert_empty() + .unwrap_or_else(|_| panic!("{context} native trace transcript had trailing bytes")); + guard + .verify(¶ms.verifier_params()) + .unwrap_or_else(|err| panic!("{context} native trace guard failed: {err:?}")); let rust_trace = solidity_trace::take(); let trace_artifacts = generator @@ -721,7 +808,7 @@ fn assert_shape_fuzz_trace_matches_native_midfall( }, ..RenderOptions::default() }) - .unwrap_or_else(|err| panic!("shape fuzz `{}` trace render failed: {err:?}", case.name)); + .unwrap_or_else(|err| panic!("{context} trace render failed: {err:?}")); let trace_verifier_solidity = trace_artifacts.verifier; let trace_vk_solidity = trace_artifacts.verifying_key.expect("trace separate render includes VK"); @@ -734,16 +821,13 @@ fn assert_shape_fuzz_trace_matches_native_midfall( let expected_true = [vec![0u8; 31], vec![1]].concat(); assert_eq!( output, expected_true, - "shape fuzz `{}` trace verifier should accept the proof", - case.name + "{context} trace verifier should accept the proof" ); - let mut rust_by_id = BTreeMap::new(); for event in rust_trace { assert!( rust_by_id.insert(event.id, (event.name, event.data)).is_none(), - "shape fuzz `{}` duplicate Rust trace id {}", - case.name, + "{context} duplicate Rust trace id {}", event.id ); } @@ -753,7 +837,7 @@ fn assert_shape_fuzz_trace_matches_native_midfall( .chain(solidity_trace.keys()) .any(|id| (60_000..61_000).contains(id)); assert_trace_equivalence_and_required_coverage( - case.name, + context, &rust_by_id, &solidity_trace, has_selector_folds, @@ -894,6 +978,36 @@ fn pbt_separate_vk_digest_prefix_affects_verification() { .unwrap(); } +#[test] +fn pbt_structured_proof_calldata_mutations_are_rejected() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let mut runner = new_property_test_runner(); + runner + .run(&any::(), |seed| { + run_structured_proof_calldata_mutation_case(seed); + Ok(()) + }) + .unwrap(); +} + +#[test] +fn pbt_proof_layout_repack_and_reader_stay_in_sync() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let mut runner = new_property_test_runner(); + runner + .run(&any::(), |seed| { + run_proof_layout_repack_reader_case(seed); + Ok(()) + }) + .unwrap(); +} + #[test] fn malformed_embedded_calldata_variants_are_rejected() { if !poseidon_inputs_available_for_evm() { @@ -1101,6 +1215,30 @@ fn production_renders_do_not_emit_gas_checkpoints() { } } +#[test] +fn production_separate_verifier_accepts_valid_proof_under_staticcall() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture = create_property_poseidon_fixture(); + let mut deployed = deployed_separate_verifier(&fixture); + assert_deployed_call_accepts(&mut deployed, &fixture, &fixture.proof, "valid proof"); + + let harness_address = deployed.evm.create_with_address_arg( + compile_solidity(STATICCALL_VERIFIER_HARNESS), + deployed.verifier_address, + ); + deployed.verifier_address = harness_address; + + let verifier_calldata = encode_calldata(&fixture.proof, &fixture.instances); + let harness_calldata = encode_bytes_call("check(bytes)", &verifier_calldata); + assert_solidity_accepts( + call_deployed_verifier_raw(&mut deployed, harness_calldata), + "valid proof through STATICCALL harness", + ); +} + #[test] fn standard_plonk_render_is_deterministic_for_same_seed() { if !poseidon_inputs_available_for_evm() { @@ -1478,6 +1616,7 @@ impl Relation for PoseidonExample { struct PropertyPoseidonFixture { compressed_proof: Vec, proof: Vec, + proof_layout: crate::lowering::abi::ProofCalldataLayout, scalar_layout: crate::lowering::RepackedProofScalarLayout, instances: Vec, params_verifier: PoseidonVerifierParams, @@ -1687,6 +1826,7 @@ fn load_property_poseidon_fixture() -> PropertyPoseidonFixture { let trace_verifier_solidity = trace_artifacts.verifier; let trace_vk_solidity = trace_artifacts.verifying_key.expect("trace separate render includes VK"); + let proof_layout = generator.inputs().lowering_plan().proof_layout; let proof = generator.repack_proof(&compressed_proof).expect("proof repack"); let scalar_layout = generator.repacked_proof_scalar_layout_for_test(); let params_verifier = srs.verifier_params(); @@ -1694,6 +1834,7 @@ fn load_property_poseidon_fixture() -> PropertyPoseidonFixture { PropertyPoseidonFixture { compressed_proof, proof, + proof_layout, scalar_layout, instances: vec![instance], params_verifier, @@ -1795,6 +1936,53 @@ fn every_proof_scalar_rejects_boundary_values() { } } +#[test] +fn every_proof_scalar_rejects_high_bit_noncanonical_values() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture = create_property_poseidon_fixture(); + let scalar_offsets = proof_scalar_offsets(&fixture); + assert!( + !scalar_offsets.is_empty(), + "fixture proof should expose scalar fields to mutate" + ); + + let mut evm = deployed_separate_verifier(&fixture); + if !deployed_call_accepts(&mut evm, &fixture, &fixture.proof, "valid proof") { + return; + } + + for (scalar_idx, (name, offset)) in scalar_offsets.into_iter().enumerate() { + let mut high_bit = [0u8; 32]; + high_bit[0] = 0x80; + + let mut deterministic_noncanonical = [0u8; 32]; + for (byte_idx, byte) in deterministic_noncanonical.iter_mut().enumerate() { + *byte = (scalar_idx as u8) + .wrapping_mul(31) + .wrapping_add((byte_idx as u8).wrapping_mul(17)) + .wrapping_add(0x42); + } + deterministic_noncanonical[0] |= 0x80; + + for (variant, word) in [ + ("high-bit-minimum", high_bit), + ("high-bit-pattern", deterministic_noncanonical), + ] { + let mut bad_proof = fixture.proof.clone(); + bad_proof[offset..offset + 0x20].copy_from_slice(&word); + assert_deployed_call_rejects( + &mut evm, + &fixture, + &bad_proof, + &format!("{name} scalar replaced with {variant} at proof offset {offset}"), + ); + } + } +} + #[test] fn representative_proof_section_mutations_are_rejected() { if !poseidon_inputs_available_for_evm() { @@ -1971,6 +2159,40 @@ fn every_proof_g1_rejects_noncanonical_coordinates() { } } +#[test] +fn every_proof_g1_rejects_base_modulus_coordinates() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture = create_property_poseidon_fixture(); + let layout = proof_g1_layout(&fixture); + let fq_modulus = bls_base_modulus_padded_coordinate(); + + let mut evm = deployed_separate_verifier(&fixture); + if !deployed_call_accepts(&mut evm, &fixture, &fixture.proof, "valid proof") { + return; + } + + for (idx, repacked_offset) in layout.repacked_offsets.iter().copied().enumerate() { + for (coordinate, coordinate_offset) in [("x", 0usize), ("y", 0x40usize)] { + let mut bad_proof = fixture.proof.clone(); + bad_proof + [repacked_offset + coordinate_offset..repacked_offset + coordinate_offset + 64] + .copy_from_slice(&fq_modulus); + assert_deployed_call_rejects( + &mut evm, + &fixture, + &bad_proof, + &format!( + "Solidity G1 {coordinate} coordinate equal to Fq modulus idx={idx} \ + repacked_offset={repacked_offset}" + ), + ); + } + } +} + #[test] fn every_proof_g1_rejects_off_curve_coordinates() { if !poseidon_inputs_available_for_evm() { @@ -2135,6 +2357,106 @@ fn run_separate_vk_digest_prefix_affects_verification_case(seed: u64) { ); } +/// Property case: start from valid calldata, then mutate one structured field +/// at a time so parser/layout bugs are exercised beyond blind byte flips. +fn run_structured_proof_calldata_mutation_case(seed: u64) { + let fixture = create_property_poseidon_fixture(); + let valid = encode_calldata(&fixture.proof, &fixture.instances); + let mutations = structured_proof_calldata_mutations(&fixture, &valid, seed); + assert!( + !mutations.is_empty(), + "structured calldata fuzzer must emit at least one mutation" + ); + + let mut deployed = deployed_separate_verifier(&fixture); + if !deployed_raw_call_accepts( + &mut deployed, + valid.clone(), + &format!("valid calldata before structured mutation fuzz seed={seed}"), + ) { + return; + } + + for mutation in mutations { + assert_solidity_rejects( + call_deployed_verifier_raw(&mut deployed, mutation.calldata), + &format!( + "structured calldata mutation seed={seed}: {}", + mutation.name + ), + ); + } +} + +/// Property case: compare the typed proof calldata layout, proof repacker, and +/// rendered Yul reader so missing reads or offset drift fail before runtime. +fn run_proof_layout_repack_reader_case(seed: u64) { + let fixture = create_property_poseidon_fixture(); + let layout = &fixture.proof_layout; + let repack_plan = + crate::lowering::quotient_numerator::vm::RepackedProofLayoutPlan::from_proof_layout(layout); + + assert_eq!( + layout.proof_cptr, + proof_payload_word_start(), + "layout proof pointer must match the ABI bytes payload start" + ); + assert_eq!( + layout.proof_len, + fixture.proof.len(), + "layout proof length must match repacked proof bytes" + ); + assert_eq!( + layout.proof_end, + instances_len_word_start(&fixture.proof), + "layout proof end must land on the instances length word" + ); + assert_eq!( + repack_plan.repacked_len(), + fixture.proof.len(), + "repack proof length must match ProofCalldataLayout" + ); + assert_eq!( + repack_plan.compressed_len(), + fixture.compressed_proof.len(), + "native compressed proof length must match repack plan" + ); + assert_eq!( + fixture.scalar_layout.eval_offset, + layout.evals.start - layout.proof_cptr, + "eval scalar offset drift between repack plan and proof layout" + ); + assert_eq!( + fixture.scalar_layout.num_evals, layout.evals.item_count, + "eval scalar count drift between repack plan and proof layout" + ); + assert_eq!( + fixture.scalar_layout.q_eval_offset, + layout.q_evals.start - layout.proof_cptr, + "q_eval scalar offset drift between repack plan and proof layout" + ); + assert_eq!( + fixture.scalar_layout.num_point_sets, layout.q_evals.item_count, + "q_eval scalar count drift between repack plan and proof layout" + ); + assert_eq!( + proof_g1_layout(&fixture).repacked_offsets, + proof_layout_g1_offsets(layout), + "G1 proof offsets must be identical in the repacker and typed layout" + ); + + let sections = proof_layout_reader_sections(layout); + proof_reader_sections_cover_exactly_once(layout, §ions).unwrap_or_else(|err| { + panic!("ProofCalldataLayout sections must cover proof bytes exactly once: {err}") + }); + assert_seeded_layout_drift_is_detected(seed, layout, §ions); + assert_rendered_reader_matches_proof_layout( + &fixture.separate_verifier_solidity, + layout, + fixture.instances.len(), + ); +} + /// Compile, deploy, and call an embedded-VK verifier. fn call_embedded_verifier( verifier_solidity: &str, @@ -2421,24 +2743,445 @@ struct ProofG1Layout { repacked_offsets: Vec, } -/// Return named scalar-word offsets in the Solidity-facing proof. -fn proof_scalar_offsets(fixture: &PropertyPoseidonFixture) -> Vec<(String, usize)> { - let layout = fixture.scalar_layout; - let mut offsets = Vec::with_capacity(layout.num_evals + layout.num_point_sets); - offsets.extend( - (0..layout.num_evals).map(|idx| (format!("eval[{idx}]"), layout.eval_offset + idx * 0x20)), +#[derive(Clone, Debug)] +struct CalldataMutation { + name: String, + calldata: Vec, +} + +#[derive(Clone, Debug)] +struct ProofLayoutSection { + name: String, + start: usize, + end: usize, +} + +/// Return named proof sections in the order the generated reader consumes them. +fn proof_layout_reader_sections( + layout: &crate::lowering::abi::ProofCalldataLayout, +) -> Vec { + let mut sections = Vec::new(); + for (phase, section) in layout.advice_phases.iter().enumerate() { + push_proof_layout_section( + &mut sections, + format!("advice phase {phase}"), + section.start, + section.end(), + ); + } + push_proof_layout_section( + &mut sections, + "lookup multiplicities", + layout.lookup_multiplicities.start, + layout.lookup_multiplicities.end(), + ); + push_proof_layout_section( + &mut sections, + "permutation products", + layout.permutation_products.start, + layout.permutation_products.end(), + ); + for lookup in &layout.lookups { + push_proof_layout_section( + &mut sections, + format!("lookup {} helpers", lookup.lookup), + lookup.helpers.start, + lookup.helpers.end(), + ); + push_proof_layout_section( + &mut sections, + format!("lookup {} accumulator", lookup.lookup), + lookup.accumulator.start, + lookup.accumulator.end(), + ); + } + push_proof_layout_section( + &mut sections, + "trash", + layout.trash.start, + layout.trash.end(), + ); + push_proof_layout_section( + &mut sections, + "quotient limbs", + layout.quotient_limbs.start, + layout.quotient_limbs.end(), + ); + push_proof_layout_section( + &mut sections, + "evals", + layout.evals.start, + layout.evals.end(), + ); + push_proof_layout_section( + &mut sections, + "f_com", + layout.f_com.start, + layout.f_com.end(), + ); + push_proof_layout_section( + &mut sections, + "q_evals", + layout.q_evals.start, + layout.q_evals.end(), + ); + push_proof_layout_section(&mut sections, "pi", layout.pi.start, layout.pi.end()); + sections +} + +/// Push one section range into a proof layout checker plan. +fn push_proof_layout_section( + sections: &mut Vec, + name: impl Into, + start: usize, + end: usize, +) { + sections.push(ProofLayoutSection { + name: name.into(), + start, + end, + }); +} + +/// Confirm the planned proof-reader sections cover each proof byte once. +fn proof_reader_sections_cover_exactly_once( + layout: &crate::lowering::abi::ProofCalldataLayout, + sections: &[ProofLayoutSection], +) -> Result<(), String> { + let mut owners: Vec> = vec![None; layout.proof_len]; + for (section_idx, section) in sections.iter().enumerate() { + if section.start > section.end { + return Err(format!( + "{} has inverted range {}..{}", + section.name, section.start, section.end + )); + } + if section.start < layout.proof_cptr || section.end > layout.proof_end { + return Err(format!( + "{} escapes proof range: {}..{} outside {}..{}", + section.name, section.start, section.end, layout.proof_cptr, layout.proof_end + )); + } + for byte in section.start - layout.proof_cptr..section.end - layout.proof_cptr { + if let Some(previous_idx) = owners[byte] { + return Err(format!( + "{} byte {byte:#x} overlaps {}", + section.name, sections[previous_idx].name + )); + } + owners[byte] = Some(section_idx); + } + } + if let Some(byte) = owners.iter().position(Option::is_none) { + return Err(format!("proof byte {byte:#x} is not read by any section")); + } + Ok(()) +} + +/// Mutate one planned section boundary and assert the coverage checker notices. +fn assert_seeded_layout_drift_is_detected( + seed: u64, + layout: &crate::lowering::abi::ProofCalldataLayout, + sections: &[ProofLayoutSection], +) { + let non_empty = sections + .iter() + .enumerate() + .filter_map(|(idx, section)| (section.start < section.end).then_some(idx)) + .collect::>(); + let drift_idx = non_empty[choose_index(seed.rotate_left(7), non_empty.len())]; + let mut drifted = sections.to_vec(); + if seed & 1 == 0 { + drifted[drift_idx].end -= 1; + } else { + drifted[drift_idx].end += 1; + } + assert!( + proof_reader_sections_cover_exactly_once(layout, &drifted).is_err(), + "seeded layout drift in {} was not detected", + sections[drift_idx].name + ); +} + +/// Return all G1 offsets in the Solidity-facing proof according to layout. +fn proof_layout_g1_offsets(layout: &crate::lowering::abi::ProofCalldataLayout) -> Vec { + let mut offsets = Vec::new(); + for section in &layout.advice_phases { + append_section_item_offsets( + &mut offsets, + layout, + section.start, + section.item_count, + section.item_bytes, + ); + } + append_section_item_offsets( + &mut offsets, + layout, + layout.lookup_multiplicities.start, + layout.lookup_multiplicities.item_count, + layout.lookup_multiplicities.item_bytes, + ); + append_section_item_offsets( + &mut offsets, + layout, + layout.permutation_products.start, + layout.permutation_products.item_count, + layout.permutation_products.item_bytes, + ); + for lookup in &layout.lookups { + append_section_item_offsets( + &mut offsets, + layout, + lookup.helpers.start, + lookup.helpers.item_count, + lookup.helpers.item_bytes, + ); + append_section_item_offsets( + &mut offsets, + layout, + lookup.accumulator.start, + lookup.accumulator.item_count, + lookup.accumulator.item_bytes, + ); + } + append_section_item_offsets( + &mut offsets, + layout, + layout.trash.start, + layout.trash.item_count, + layout.trash.item_bytes, + ); + append_section_item_offsets( + &mut offsets, + layout, + layout.quotient_limbs.start, + layout.quotient_limbs.item_count, + layout.quotient_limbs.item_bytes, + ); + append_section_item_offsets( + &mut offsets, + layout, + layout.f_com.start, + layout.f_com.item_count, + layout.f_com.item_bytes, + ); + append_section_item_offsets( + &mut offsets, + layout, + layout.pi.start, + layout.pi.item_count, + layout.pi.item_bytes, + ); + offsets +} + +/// Append proof-relative offsets for homogeneous section items. +fn append_section_item_offsets( + offsets: &mut Vec, + layout: &crate::lowering::abi::ProofCalldataLayout, + start: usize, + item_count: usize, + item_bytes: usize, +) { + offsets.extend((0..item_count).map(|idx| start - layout.proof_cptr + idx * item_bytes)); +} + +/// Confirm the generated Solidity reader literals agree with the layout. +fn assert_rendered_reader_matches_proof_layout( + solidity: &str, + layout: &crate::lowering::abi::ProofCalldataLayout, + num_instances: usize, +) { + assert!( + solidity.contains(&format!( + "PROOF_LEN_CPTR = {};", + yul_hex(proof_len_word_start()) + )), + "rendered constants must expose the proof length word pointer" + ); + assert!( + solidity.contains(&format!("PROOF_CPTR = {};", yul_hex(layout.proof_cptr))), + "rendered constants must expose ProofCalldataLayout::proof_cptr" + ); + assert!( + solidity.contains(&format!( + "NUM_INSTANCE_CPTR = {};", + yul_hex(layout.proof_end) + )), + "rendered constants must expose ProofCalldataLayout::proof_end" + ); + assert!( + solidity.contains(&format!( + "eq({}, calldataload(PROOF_LEN_CPTR))", + yul_hex(layout.proof_len) + )), + "rendered proof length guard must use ProofCalldataLayout::proof_len" + ); + assert!( + solidity.contains(&format!( + "eq({}, calldataload(NUM_INSTANCE_CPTR))", + num_instances + )), + "rendered instance length guard must use fixture instance count" + ); + + let reader = rendered_proof_reader_block(solidity); + assert_eq!( + rendered_proof_reader_loop_lengths(reader), + expected_reader_loop_lengths(layout), + "generated proof reader loop lengths drifted from ProofCalldataLayout" ); - offsets.extend( - (0..layout.num_point_sets) - .map(|idx| (format!("q_eval[{idx}]"), layout.q_eval_offset + idx * 0x20)), + assert_eq!( + rendered_proof_reader_increment_lengths(reader), + expected_reader_increment_lengths(layout), + "generated proof reader proof_cptr increments drifted from ProofCalldataLayout" ); + assert!( + solidity.contains("if iszero(eq(proof_cptr, NUM_INSTANCE_CPTR)) { revert(0, 0) }"), + "generated proof reader must fail closed if proof_cptr drifts" + ); +} + +/// Slice the rendered source down to the proof parser block. +fn rendered_proof_reader_block(solidity: &str) -> &str { + let start = solidity + .find("let proof_cptr := PROOF_CPTR") + .expect("rendered verifier contains proof parser start"); + let end = solidity + .find("if iszero(eq(proof_cptr, NUM_INSTANCE_CPTR))") + .expect("rendered verifier contains proof parser end guard"); + &solidity[start..end] +} + +/// Extract `for { let end := add(proof_cptr, N) }` lengths from Yul. +fn rendered_proof_reader_loop_lengths(reader: &str) -> Vec { + const NEEDLE: &str = "for { let end := add(proof_cptr, "; + let mut lengths = Vec::new(); + let mut rest = reader; + while let Some(pos) = rest.find(NEEDLE) { + let after = &rest[pos + NEEDLE.len()..]; + let close = after.find(')').expect("proof reader loop has a closing paren"); + lengths.push(parse_yul_usize(after[..close].trim())); + rest = &after[close..]; + } + lengths +} + +/// Extract every rendered `proof_cptr` increment stride from the proof reader. +fn rendered_proof_reader_increment_lengths(reader: &str) -> Vec { + const NEEDLE: &str = "proof_cptr := add(proof_cptr, "; + let mut lengths = Vec::new(); + let mut rest = reader; + while let Some(pos) = rest.find(NEEDLE) { + let after = &rest[pos + NEEDLE.len()..]; + let close = after.find(')').expect("proof reader increment has a closing paren"); + lengths.push(parse_yul_usize(after[..close].trim())); + rest = &after[close..]; + } + lengths +} + +/// Expected loop byte lengths in the generated proof reader. +fn expected_reader_loop_lengths(layout: &crate::lowering::abi::ProofCalldataLayout) -> Vec { + let mut lengths = + layout.advice_phases.iter().map(|section| section.byte_len).collect::>(); + if layout.lookup_multiplicities.item_count != 0 { + lengths.push(layout.lookup_multiplicities.byte_len); + } + if layout.permutation_products.item_count != 0 { + lengths.push(layout.permutation_products.byte_len); + } + if !layout.lookups.is_empty() { + lengths.extend(layout.lookups.iter().map(|lookup| lookup.helpers.byte_len)); + } + if layout.trash.item_count != 0 { + lengths.push(layout.trash.byte_len); + } + lengths.push(layout.quotient_limbs.byte_len); + lengths.push(layout.evals.byte_len); + lengths.push(layout.q_evals.byte_len); + lengths +} + +/// Expected `proof_cptr` increment strides in the generated proof reader. +fn expected_reader_increment_lengths( + layout: &crate::lowering::abi::ProofCalldataLayout, +) -> Vec { + let mut lengths = layout + .advice_phases + .iter() + .map(|section| section.item_bytes) + .collect::>(); + if layout.lookup_multiplicities.item_count != 0 { + lengths.push(layout.lookup_multiplicities.item_bytes); + } + if layout.permutation_products.item_count != 0 { + lengths.push(layout.permutation_products.item_bytes); + } + if !layout.lookups.is_empty() { + for lookup in &layout.lookups { + lengths.push(lookup.helpers.item_bytes); + lengths.push(lookup.accumulator.item_bytes); + } + } + if layout.trash.item_count != 0 { + lengths.push(layout.trash.item_bytes); + } + lengths.push(layout.quotient_limbs.item_bytes); + lengths.push(layout.evals.item_bytes); + lengths.push(layout.f_com.item_bytes); + lengths.push(layout.q_evals.item_bytes); + lengths.push(layout.pi.item_bytes); + lengths +} + +/// Render a Yul/Solidity integer literal the way the templates do. +fn yul_hex(value: usize) -> String { + format!("0x{value:x}") +} + +/// Parse a simple Yul integer literal. +fn parse_yul_usize(literal: &str) -> usize { + literal + .strip_prefix("0x") + .map(|hex| usize::from_str_radix(hex, 16).expect("valid hex literal")) + .unwrap_or_else(|| literal.parse().expect("valid decimal literal")) +} + +/// Return ordinary evaluation scalar offsets in the Solidity-facing proof. +fn proof_eval_offsets(fixture: &PropertyPoseidonFixture) -> Vec<(String, usize)> { + let layout = fixture.scalar_layout; + let offsets = (0..layout.num_evals) + .map(|idx| (format!("eval[{idx}]"), layout.eval_offset + idx * 0x20)) + .collect::>(); assert!( offsets.iter().all(|(_, offset)| offset + 0x20 <= fixture.proof.len()), - "scalar offsets must be inside the Solidity proof" + "eval offsets must be inside the Solidity proof" ); offsets } +/// Return quotient-opening evaluation offsets in the Solidity-facing proof. +fn proof_q_eval_offsets(fixture: &PropertyPoseidonFixture) -> Vec<(String, usize)> { + let layout = fixture.scalar_layout; + let offsets = (0..layout.num_point_sets) + .map(|idx| (format!("q_eval[{idx}]"), layout.q_eval_offset + idx * 0x20)) + .collect::>(); + assert!( + offsets.iter().all(|(_, offset)| offset + 0x20 <= fixture.proof.len()), + "q_eval offsets must be inside the Solidity proof" + ); + offsets +} + +/// Return named scalar-word offsets in the Solidity-facing proof. +fn proof_scalar_offsets(fixture: &PropertyPoseidonFixture) -> Vec<(String, usize)> { + let mut offsets = proof_eval_offsets(fixture); + offsets.extend(proof_q_eval_offsets(fixture)); + offsets +} + /// Return compressed and repacked G1 offsets for proof mutation tests. fn proof_g1_layout(fixture: &PropertyPoseidonFixture) -> ProofG1Layout { let prefix_g1_count = fixture.scalar_layout.eval_offset / 0x80; @@ -2535,6 +3278,167 @@ fn assert_solidity_rejects(output: Result, ()>, context: &str) { ); } +/// Generate structured one-field mutations from a valid `verifyProof` calldata. +fn structured_proof_calldata_mutations( + fixture: &PropertyPoseidonFixture, + valid: &[u8], + seed: u64, +) -> Vec { + let mut mutations = Vec::new(); + let proof_start = proof_payload_word_start(); + + let g1_layout = proof_g1_layout(fixture); + let g1_idx = choose_index(seed, g1_layout.repacked_offsets.len()); + let g1_offset = g1_layout.repacked_offsets[g1_idx]; + mutations.push(mutate_calldata_byte( + valid, + format!("proof G1[{g1_idx}] byte flip"), + proof_start + g1_offset + choose_index(seed.rotate_left(7), 0x80), + seed, + )); + + let eval_offsets = proof_eval_offsets(fixture); + let eval_idx = choose_index(seed.rotate_left(11), eval_offsets.len()); + let (eval_name, eval_offset) = &eval_offsets[eval_idx]; + mutations.push(mutate_calldata_byte( + valid, + format!("proof scalar {eval_name} byte flip"), + proof_start + *eval_offset + choose_index(seed.rotate_left(13), 0x20), + seed.rotate_left(17), + )); + + let q_eval_offsets = proof_q_eval_offsets(fixture); + let q_eval_idx = choose_index(seed.rotate_left(19), q_eval_offsets.len()); + let (q_eval_name, q_eval_offset) = &q_eval_offsets[q_eval_idx]; + mutations.push(mutate_calldata_byte( + valid, + format!("proof q_eval {q_eval_name} byte flip"), + proof_start + *q_eval_offset + choose_index(seed.rotate_left(23), 0x20), + seed.rotate_left(29), + )); + + let instance_word_start = first_instance_word_start(&fixture.proof) + + choose_index(seed.rotate_left(31), fixture.instances.len()) * 0x20; + mutations.push(mutate_calldata_byte( + valid, + "public instance word byte flip", + instance_word_start + choose_index(seed.rotate_left(37), 0x20), + seed.rotate_left(41), + )); + + let mut dynamic_head = valid.to_vec(); + if seed & 1 == 0 { + overwrite_u256_word(&mut dynamic_head, 0x04, 0x20); + mutations.push(CalldataMutation { + name: "proof dynamic ABI head overlaps static head".to_string(), + calldata: dynamic_head, + }); + } else { + overwrite_u256_word( + &mut dynamic_head, + 0x24, + canonical_instances_head(&fixture.proof) as u64 + 0x20, + ); + mutations.push(CalldataMutation { + name: "instances dynamic ABI head shifted away from proof tail".to_string(), + calldata: dynamic_head, + }); + } + + let mut trailing = valid.to_vec(); + let trailing_len = 1 + choose_index(seed.rotate_left(43), 0x20); + trailing.extend( + (0..trailing_len) + .map(|idx| (seed as u8).wrapping_add(idx as u8).wrapping_mul(17).wrapping_add(1)), + ); + mutations.push(CalldataMutation { + name: format!("trailing bytes len={trailing_len}"), + calldata: trailing, + }); + + let mut length_mutation = valid.to_vec(); + if seed & 2 == 0 { + let proof_len = fixture.proof.len(); + let mutated_len = if seed & 4 == 0 { + proof_len.saturating_sub(1) + } else { + proof_len + 0x20 + }; + overwrite_u256_word( + &mut length_mutation, + proof_len_word_start(), + mutated_len as u64, + ); + mutations.push(CalldataMutation { + name: format!("proof length word changed to {mutated_len}"), + calldata: length_mutation, + }); + } else { + let mutated_len = if seed & 4 == 0 { + 0 + } else { + fixture.instances.len() + 1 + }; + overwrite_u256_word( + &mut length_mutation, + instances_len_word_start(&fixture.proof), + mutated_len as u64, + ); + mutations.push(CalldataMutation { + name: format!("instances length word changed to {mutated_len}"), + calldata: length_mutation, + }); + } + + mutations.push(mutate_calldata_byte( + valid, + "function selector byte flip", + choose_index(seed.rotate_left(47), FN_SIG_VERIFY_PROOF.len()), + seed.rotate_left(53), + )); + + let representative_offsets = representative_proof_section_offsets(fixture); + let mut shifted_proof = fixture.proof.clone(); + let insertion_offset = + representative_offsets[choose_index(seed.rotate_left(59), representative_offsets.len())].1; + shifted_proof.splice( + insertion_offset..insertion_offset, + [0x80 | (seed as u8), seed.rotate_left(8) as u8], + ); + mutations.push(CalldataMutation { + name: format!("proof field offsets shifted at proof offset {insertion_offset}"), + calldata: encode_calldata(&shifted_proof, &fixture.instances), + }); + + mutations +} + +/// Pick a deterministic index from a fuzz seed. +fn choose_index(seed: u64, len: usize) -> usize { + assert!(len != 0, "cannot choose from an empty set"); + seed as usize % len +} + +/// Flip one non-zero bit in a calldata byte. +fn mutate_calldata_byte( + valid: &[u8], + name: impl Into, + byte_offset: usize, + seed: u64, +) -> CalldataMutation { + assert!( + byte_offset < valid.len(), + "calldata byte mutation offset {byte_offset} outside len {}", + valid.len() + ); + let mut calldata = valid.to_vec(); + calldata[byte_offset] ^= 1u8 << (seed as u8 & 7); + CalldataMutation { + name: name.into(), + calldata, + } +} + /// Flip one nibble in a large hex literal for broad source-mutation tests. fn mutate_first_large_hex_literal(solidity: &str, ordinal_seed: usize) -> String { let bytes = solidity.as_bytes(); @@ -2668,6 +3572,32 @@ fn fr_modulus_u256() -> U256 { U256::from_be_bytes(fr_modulus_be_word()) } +/// Return the BLS12-381 base-field modulus in padded EIP-2537 coordinate form. +fn bls_base_modulus_padded_coordinate() -> [u8; 64] { + let fq = hex::decode( + "1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f624\ + 1eabfffeb153ffffb9feffffffffaaab", + ) + .expect("Fq modulus hex"); + assert_eq!(fq.len(), 48, "BLS12-381 Fq modulus must be 48 bytes"); + + let mut out = [0u8; 64]; + out[16..64].copy_from_slice(&fq); + out +} + +/// ABI-encode a single `bytes` argument call. +fn encode_bytes_call(signature: &str, payload: &[u8]) -> Vec { + let padding = (32 - payload.len() % 32) % 32; + let mut out = Vec::with_capacity(4 + 0x40 + payload.len() + padding); + out.extend_from_slice(&sha3::Keccak256::digest(signature)[..4]); + out.extend_from_slice(&u256_word(0x20)); + out.extend_from_slice(&u256_word(payload.len() as u64)); + out.extend_from_slice(payload); + out.resize(out.len() + padding, 0); + out +} + /// Append a named proof section offset if that offset is not already covered. fn push_unique_section( sections: &mut Vec<(String, usize)>, @@ -2756,9 +3686,24 @@ fn canonical_instances_head(proof: &[u8]) -> usize { 0x40 + 0x20 + proof.len() } +/// Calldata byte offset of the proof length word. +fn proof_len_word_start() -> usize { + 4 + 0x40 +} + +/// Calldata byte offset of the first proof payload word. +fn proof_payload_word_start() -> usize { + proof_len_word_start() + 0x20 +} + +/// Calldata byte offset of the instance array length word. +fn instances_len_word_start(proof: &[u8]) -> usize { + 4 + canonical_instances_head(proof) +} + /// Calldata byte offset of the first instance word. fn first_instance_word_start(proof: &[u8]) -> usize { - 4 + canonical_instances_head(proof) + 0x20 + instances_len_word_start(proof) + 0x20 } /// Build malformed calldata with valid payloads but shifted dynamic heads. From 5955ee46be96cf7c79367c533b164d054cd3f5c3 Mon Sep 17 00:00:00 2001 From: Julien Coolen Date: Tue, 19 May 2026 10:05:12 +0100 Subject: [PATCH 07/19] Patch blake2b_halo2 transcript inference --- Cargo.toml | 3 + .../blake2b_halo2/.github/workflows/rust.yml | 22 + third_party/blake2b_halo2/.gitignore | 3 + third_party/blake2b_halo2/.rustfmt.toml | 6 + third_party/blake2b_halo2/Cargo.toml | 50 + third_party/blake2b_halo2/LICENSE | 201 ++ third_party/blake2b_halo2/README.md | 54 + .../blake2b_halo2/benches/mocked_proving.rs | 43 + .../blake2b_halo2/benches/pk_generation.rs | 40 + .../blake2b_halo2/benches/proof_generation.rs | 45 + third_party/blake2b_halo2/benches/utils.rs | 48 + .../blake2b_halo2/benches/verification.rs | 44 + .../blake2b_halo2/benches/vk_generation.rs | 45 + .../blake2b_halo2/examples/inputs.json | 5 + .../blake2b_halo2/examples/interface.rs | 112 + third_party/blake2b_halo2/rust-toolchain.toml | 3 + .../src/base_operations/addition_mod_64.rs | 141 + .../base_operations/generic_limb_rotation.rs | 76 + .../blake2b_halo2/src/base_operations/mod.rs | 219 ++ .../src/base_operations/negate.rs | 70 + .../src/base_operations/rotate_63.rs | 91 + .../blake2b_halo2/src/base_operations/xor.rs | 201 ++ .../blake2b_halo2/src/blake2b/blake2b_chip.rs | 733 ++++ .../src/blake2b/blake2b_instructions.rs | 84 + third_party/blake2b_halo2/src/blake2b/mod.rs | 22 + .../blake2b_halo2/src/blake2b/utils.rs | 166 + third_party/blake2b_halo2/src/lib.rs | 19 + third_party/blake2b_halo2/src/tests/mod.rs | 151 + .../test_blake2b/circuit_in_production.rs | 25 + .../src/tests/test_blake2b/mod.rs | 7 + .../src/tests/test_blake2b/smoke_tests.rs | 99 + .../test_blake2b/variable_key_length_tests.rs | 16 + .../variable_output_length_tests.rs | 90 + .../src/tests/test_blake2b/vector_tests.rs | 66 + .../src/tests/test_negate/mod.rs | 38 + .../src/tests/test_negate/negate_circuit.rs | 106 + .../addition_mod_64_circuit_8bits.rs | 76 + ...tion_mod_64_circuit_8bits_autogenerated.rs | 174 + .../src/tests/tests_addition/mod.rs | 50 + ...ddition_mod_64_with_autogenerated_trace.rs | 49 + ...st_addition_mod_64_with_processed_trace.rs | 153 + .../tests_rotation/limb_rotation_circuit.rs | 119 + .../limb_rotation_circuit_autogenerated.rs | 121 + .../src/tests/tests_rotation/mod.rs | 59 + .../rotation_63_circuit_8bit_limbs.rs | 67 + ...ion_63_cirtuit_8bit_limbs_autogenerated.rs | 118 + .../test_limb_rotation_16_24_32.rs | 180 + ...st_limb_rotation_16_24_32_autogenerated.rs | 66 + .../test_rotation_63_8_bit_limbs.rs | 40 + ...t_rotation_63_8_bit_limbs_autogenerated.rs | 24 + .../blake2b_halo2/src/tests/tests_xor/mod.rs | 6 + .../tests_xor_with_generated_trace.rs | 30 + .../tests_xor_with_processed_trace.rs | 149 + .../src/tests/tests_xor/xor_circuit.rs | 106 + .../tests_xor/xor_circuit_autogenerated.rs | 136 + third_party/blake2b_halo2/src/types/bit.rs | 66 + .../blake2b_halo2/src/types/blake2b_word.rs | 134 + third_party/blake2b_halo2/src/types/byte.rs | 117 + third_party/blake2b_halo2/src/types/mod.rs | 42 + third_party/blake2b_halo2/src/types/row.rs | 21 + .../src/usage_utils/blake2b_circuit.rs | 126 + .../src/usage_utils/circuit_runner.rs | 188 + .../blake2b_halo2/src/usage_utils/mod.rs | 6 + third_party/blake2b_halo2/test_vector.json | 3074 +++++++++++++++++ 64 files changed, 8641 insertions(+) create mode 100644 third_party/blake2b_halo2/.github/workflows/rust.yml create mode 100644 third_party/blake2b_halo2/.gitignore create mode 100644 third_party/blake2b_halo2/.rustfmt.toml create mode 100644 third_party/blake2b_halo2/Cargo.toml create mode 100644 third_party/blake2b_halo2/LICENSE create mode 100644 third_party/blake2b_halo2/README.md create mode 100644 third_party/blake2b_halo2/benches/mocked_proving.rs create mode 100644 third_party/blake2b_halo2/benches/pk_generation.rs create mode 100644 third_party/blake2b_halo2/benches/proof_generation.rs create mode 100644 third_party/blake2b_halo2/benches/utils.rs create mode 100644 third_party/blake2b_halo2/benches/verification.rs create mode 100644 third_party/blake2b_halo2/benches/vk_generation.rs create mode 100644 third_party/blake2b_halo2/examples/inputs.json create mode 100644 third_party/blake2b_halo2/examples/interface.rs create mode 100644 third_party/blake2b_halo2/rust-toolchain.toml create mode 100644 third_party/blake2b_halo2/src/base_operations/addition_mod_64.rs create mode 100644 third_party/blake2b_halo2/src/base_operations/generic_limb_rotation.rs create mode 100644 third_party/blake2b_halo2/src/base_operations/mod.rs create mode 100644 third_party/blake2b_halo2/src/base_operations/negate.rs create mode 100644 third_party/blake2b_halo2/src/base_operations/rotate_63.rs create mode 100644 third_party/blake2b_halo2/src/base_operations/xor.rs create mode 100644 third_party/blake2b_halo2/src/blake2b/blake2b_chip.rs create mode 100644 third_party/blake2b_halo2/src/blake2b/blake2b_instructions.rs create mode 100644 third_party/blake2b_halo2/src/blake2b/mod.rs create mode 100644 third_party/blake2b_halo2/src/blake2b/utils.rs create mode 100644 third_party/blake2b_halo2/src/lib.rs create mode 100644 third_party/blake2b_halo2/src/tests/mod.rs create mode 100644 third_party/blake2b_halo2/src/tests/test_blake2b/circuit_in_production.rs create mode 100644 third_party/blake2b_halo2/src/tests/test_blake2b/mod.rs create mode 100644 third_party/blake2b_halo2/src/tests/test_blake2b/smoke_tests.rs create mode 100644 third_party/blake2b_halo2/src/tests/test_blake2b/variable_key_length_tests.rs create mode 100644 third_party/blake2b_halo2/src/tests/test_blake2b/variable_output_length_tests.rs create mode 100644 third_party/blake2b_halo2/src/tests/test_blake2b/vector_tests.rs create mode 100644 third_party/blake2b_halo2/src/tests/test_negate/mod.rs create mode 100644 third_party/blake2b_halo2/src/tests/test_negate/negate_circuit.rs create mode 100644 third_party/blake2b_halo2/src/tests/tests_addition/addition_mod_64_circuit_8bits.rs create mode 100644 third_party/blake2b_halo2/src/tests/tests_addition/addition_mod_64_circuit_8bits_autogenerated.rs create mode 100644 third_party/blake2b_halo2/src/tests/tests_addition/mod.rs create mode 100644 third_party/blake2b_halo2/src/tests/tests_addition/test_addition_mod_64_with_autogenerated_trace.rs create mode 100644 third_party/blake2b_halo2/src/tests/tests_addition/test_addition_mod_64_with_processed_trace.rs create mode 100644 third_party/blake2b_halo2/src/tests/tests_rotation/limb_rotation_circuit.rs create mode 100644 third_party/blake2b_halo2/src/tests/tests_rotation/limb_rotation_circuit_autogenerated.rs create mode 100644 third_party/blake2b_halo2/src/tests/tests_rotation/mod.rs create mode 100644 third_party/blake2b_halo2/src/tests/tests_rotation/rotation_63_circuit_8bit_limbs.rs create mode 100644 third_party/blake2b_halo2/src/tests/tests_rotation/rotation_63_cirtuit_8bit_limbs_autogenerated.rs create mode 100644 third_party/blake2b_halo2/src/tests/tests_rotation/test_limb_rotation_16_24_32.rs create mode 100644 third_party/blake2b_halo2/src/tests/tests_rotation/test_limb_rotation_16_24_32_autogenerated.rs create mode 100644 third_party/blake2b_halo2/src/tests/tests_rotation/test_rotation_63_8_bit_limbs.rs create mode 100644 third_party/blake2b_halo2/src/tests/tests_rotation/test_rotation_63_8_bit_limbs_autogenerated.rs create mode 100644 third_party/blake2b_halo2/src/tests/tests_xor/mod.rs create mode 100644 third_party/blake2b_halo2/src/tests/tests_xor/tests_xor_with_generated_trace.rs create mode 100644 third_party/blake2b_halo2/src/tests/tests_xor/tests_xor_with_processed_trace.rs create mode 100644 third_party/blake2b_halo2/src/tests/tests_xor/xor_circuit.rs create mode 100644 third_party/blake2b_halo2/src/tests/tests_xor/xor_circuit_autogenerated.rs create mode 100644 third_party/blake2b_halo2/src/types/bit.rs create mode 100644 third_party/blake2b_halo2/src/types/blake2b_word.rs create mode 100644 third_party/blake2b_halo2/src/types/byte.rs create mode 100644 third_party/blake2b_halo2/src/types/mod.rs create mode 100644 third_party/blake2b_halo2/src/types/row.rs create mode 100644 third_party/blake2b_halo2/src/usage_utils/blake2b_circuit.rs create mode 100644 third_party/blake2b_halo2/src/usage_utils/circuit_runner.rs create mode 100644 third_party/blake2b_halo2/src/usage_utils/mod.rs create mode 100644 third_party/blake2b_halo2/test_vector.json diff --git a/Cargo.toml b/Cargo.toml index f32794706..7adad5a47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,9 @@ midnight-proofs = { path = "proofs" } midnight-curves = { path = "curves" } midnight-circuits = { path = "circuits" } +[patch."https://github.com/eryxcoop/blake2b_halo2"] +blake2b_halo2 = { path = "third_party/blake2b_halo2" } + ## Benchmarks [profile.bench] diff --git a/third_party/blake2b_halo2/.github/workflows/rust.yml b/third_party/blake2b_halo2/.github/workflows/rust.yml new file mode 100644 index 000000000..29b55e9fe --- /dev/null +++ b/third_party/blake2b_halo2/.github/workflows/rust.yml @@ -0,0 +1,22 @@ +name: Rust + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose --release diff --git a/third_party/blake2b_halo2/.gitignore b/third_party/blake2b_halo2/.gitignore new file mode 100644 index 000000000..65623d5a9 --- /dev/null +++ b/third_party/blake2b_halo2/.gitignore @@ -0,0 +1,3 @@ +*/target +.idea +target diff --git a/third_party/blake2b_halo2/.rustfmt.toml b/third_party/blake2b_halo2/.rustfmt.toml new file mode 100644 index 000000000..07d1f4533 --- /dev/null +++ b/third_party/blake2b_halo2/.rustfmt.toml @@ -0,0 +1,6 @@ +max_width = 100 +reorder_imports = false +reorder_modules = false +use_small_heuristics = "Max" + +struct_lit_width = 18 \ No newline at end of file diff --git a/third_party/blake2b_halo2/Cargo.toml b/third_party/blake2b_halo2/Cargo.toml new file mode 100644 index 000000000..9d8f41dbe --- /dev/null +++ b/third_party/blake2b_halo2/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "blake2b_halo2" +version = "0.1.0" +edition = "2021" +description = """ +Midnight-proofs circuit for Blake2b +""" +license = "Apache-2.0" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1" +ff = "0.13" +blake2b_simd = "1" +midnight-curves = "0.2.0" +midnight-proofs = { git = "https://github.com/midnightntwrk/midnight-zk", branch = "main", default-features = false, features = [ + "circuit-params", + "committed-instances", +] } + +rand = "0.8" +hex = "0.4.3" +num-bigint = "0.4" + +[dev-dependencies] +criterion = { version = "0.5.1", features = ["html_reports", "csv_output"] } +blake2-rfc = "0.2.18" + +[profile.bench] +lto = "fat" + +[[bench]] +name = "mocked_proving" +harness = false + +[[bench]] +name = "vk_generation" +harness = false + +[[bench]] +name = "pk_generation" +harness = false + +[[bench]] +name = "proof_generation" +harness = false + +[[bench]] +name = "verification" +harness = false diff --git a/third_party/blake2b_halo2/LICENSE b/third_party/blake2b_halo2/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/third_party/blake2b_halo2/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/third_party/blake2b_halo2/README.md b/third_party/blake2b_halo2/README.md new file mode 100644 index 000000000..9c5d28851 --- /dev/null +++ b/third_party/blake2b_halo2/README.md @@ -0,0 +1,54 @@ +# blake2b_halo2 +This repo holds an optimized Blake2b implementation in Halo2 prover. +> âš ï¸ **Security Warning** +> This code has **not been audited**. Use at your own risk. + + +* We are using [this halo2 version](https://github.com/midnightntwrk/midnight-zk) to build our circuits. +* The cargo version should be 1.84.0 or higher (no need for nightly). + + Under the directory ```blake2b_halo2``` there are Halo2 chips that implement primitives for operating modulo 2â¶â´, a chip for the Blake2b operation and tests for the above. + +## Documentation + +We have a [documentation](https://hackmd.io/@BjOWve_hTxGZidE1ii0HJg/HkVu20JFkx) where you can find more detail about +out Blake2b implementation. You can also find more detailed explanations of all our gates. + + +# Trying the implementation +The executable in ```examples/interface``` allows you to try the halo2 implementation of Blake2b. +Just fill the ```examples/inputs.json``` file with the message, key and desired output length (in bytes) and run the following command: + +```cargo run --release --example interface``` + +# Running the tests + +We have unit tests for all our auxiliar chips and the vector tests for the Blake2b implementation. All the tests should be executed on the ```blake2b_halo2``` directory. + +To run tests of our Halo2 implementation': + +```cargo test --release test_hashes_in_circuit_``` + +Those tests use the same test vector than the plain Rust implementation. Running the above tests can take some time since there are 512 tests in the test vector, and each one repeats all the static procedures (like creating big lookup tables), but it shouldn't take more than 2 minutes in release mode. + +To test the auxiliar chips: + +```cargo test --release -- --skip test_hashes_in_circuit_``` + +# Benchmarking +Just run + +```cargo bench``` + +The report should be found in ```/target/criterion/report/index.html```. + +There are 5 targets for benchmarking: mocked proving, verification key generation, proving key generation, proof generation and verification. Each one will compare all the optimizations over inputs of different size. Running all the benchmarks can take quite some time, so if you want to run one specific target use: + +```cargo bench --bench ``` + +where is one of the following: +* mocked_proving +* vk_generation +* pk_generation +* proof_generation +* verification \ No newline at end of file diff --git a/third_party/blake2b_halo2/benches/mocked_proving.rs b/third_party/blake2b_halo2/benches/mocked_proving.rs new file mode 100644 index 000000000..b17c1cd71 --- /dev/null +++ b/third_party/blake2b_halo2/benches/mocked_proving.rs @@ -0,0 +1,43 @@ +use criterion::{ + criterion_group, criterion_main, BatchSize, BenchmarkGroup, BenchmarkId, Criterion, Throughput, +}; +use criterion::measurement::WallTime; +use blake2b_halo2::usage_utils::circuit_runner::CircuitRunner; + +pub mod utils; +use utils::*; + +criterion_group!(mocked_prover, benchmark_mocked_proving); +criterion_main!(mocked_prover); + +pub fn benchmark_mocked_proving(c: &mut Criterion) { + let mut group = c.benchmark_group("optimization_comparison"); + configure_group(&mut group); + + for amount_of_blocks in benchmarking_block_sizes() { + group.throughput(Throughput::Bytes(amount_of_blocks as u64)); + + benchmark_optimization_with_amount_of_blocks(&mut group, amount_of_blocks, "opt_recycle"); + } + group.finish() +} + +fn benchmark_optimization_with_amount_of_blocks( + group: &mut BenchmarkGroup, + amount_of_blocks: usize, + optimization_name: &str, +) { + group.bench_function(BenchmarkId::new(optimization_name, amount_of_blocks), |b| { + b.iter_batched( + || { + let ci = random_input_for_desired_blocks(amount_of_blocks); + let circuit = CircuitRunner::create_circuit_for_packed_inputs(ci.clone()); + (circuit, ci.4) + }, + |(circuit, expected)| { + CircuitRunner::mock_prove_with_public_inputs_ref(&expected, &circuit) + }, + BatchSize::SmallInput, + ) + }); +} diff --git a/third_party/blake2b_halo2/benches/pk_generation.rs b/third_party/blake2b_halo2/benches/pk_generation.rs new file mode 100644 index 000000000..729e68824 --- /dev/null +++ b/third_party/blake2b_halo2/benches/pk_generation.rs @@ -0,0 +1,40 @@ +use blake2b_halo2::usage_utils::circuit_runner::CircuitRunner; +use criterion::measurement::WallTime; +use criterion::{criterion_group, criterion_main, BenchmarkGroup, BenchmarkId, Criterion, Throughput}; +use midnight_curves::bls12_381::Bls12; +use midnight_proofs::poly::kzg::params::ParamsKZG; + +pub mod utils; +use utils::*; + +criterion_group!(pk, benchmark_proving_key_generation); +criterion_main!(pk); + +pub fn benchmark_proving_key_generation(c: &mut Criterion) { + let mut group = c.benchmark_group("proving_key"); + configure_group(&mut group); + + let params = ParamsKZG::::unsafe_setup(17, &mut rand::thread_rng()); + + for amount_of_blocks in benchmarking_block_sizes() { + group.throughput(Throughput::Bytes(amount_of_blocks as u64)); + + benchmark_proving_key(¶ms, &mut group, amount_of_blocks, "opt_recycle"); + } + group.finish() +} + +fn benchmark_proving_key( + params: &ParamsKZG, + group: &mut BenchmarkGroup, + amount_of_blocks: usize, + name: &str, +) { + let ci = random_input_for_desired_blocks(amount_of_blocks); + let circuit = CircuitRunner::create_circuit_for_packed_inputs(ci); + let vk = CircuitRunner::create_vk(&circuit, params); + + group.bench_function(BenchmarkId::new(name, amount_of_blocks), |b| { + b.iter(|| CircuitRunner::create_pk(&circuit, vk.clone())) + }); +} diff --git a/third_party/blake2b_halo2/benches/proof_generation.rs b/third_party/blake2b_halo2/benches/proof_generation.rs new file mode 100644 index 000000000..a18d719af --- /dev/null +++ b/third_party/blake2b_halo2/benches/proof_generation.rs @@ -0,0 +1,45 @@ +use blake2b_halo2::usage_utils::circuit_runner::CircuitRunner; +use criterion::measurement::WallTime; +use criterion::{criterion_group, criterion_main, BenchmarkGroup, BenchmarkId, Criterion, Throughput}; +use midnight_curves::bls12_381::Bls12; +use midnight_proofs::poly::kzg::params::ParamsKZG; + +pub mod utils; +use utils::*; + +criterion_group!(proof, benchmark_proof_generation); +criterion_main!(proof); + +pub fn benchmark_proof_generation(c: &mut Criterion) { + let mut group = c.benchmark_group("proof"); + configure_group(&mut group); + + let params = ParamsKZG::::unsafe_setup(17, &mut rand::thread_rng()); + + for amount_of_blocks in benchmarking_block_sizes() { + group.throughput(Throughput::Bytes(amount_of_blocks as u64)); + + benchmark_proof(¶ms, &mut group, amount_of_blocks, "opt_recycle"); + } + group.finish() +} + +fn benchmark_proof( + params: &ParamsKZG, + group: &mut BenchmarkGroup, + amount_of_blocks: usize, + name: &str, +) { + let ci = random_input_for_desired_blocks(amount_of_blocks); + let expected_output_fields = ci.4; + + let circuit = CircuitRunner::create_circuit_for_packed_inputs(ci); + let vk = CircuitRunner::create_vk(&circuit, params); + let pk = CircuitRunner::create_pk(&circuit, vk.clone()); + + group.bench_function(BenchmarkId::new(name, amount_of_blocks), |b| { + b.iter(|| { + CircuitRunner::create_proof(&expected_output_fields, circuit.clone(), params, &pk) + }) + }); +} diff --git a/third_party/blake2b_halo2/benches/utils.rs b/third_party/blake2b_halo2/benches/utils.rs new file mode 100644 index 000000000..182ecffe1 --- /dev/null +++ b/third_party/blake2b_halo2/benches/utils.rs @@ -0,0 +1,48 @@ +use criterion::{BenchmarkGroup, SamplingMode}; +use criterion::measurement::WallTime; +use midnight_proofs::circuit::Value; +use midnight_curves::bls12_381::Fq; +use rand::Rng; +use blake2b_halo2::usage_utils::circuit_runner::Blake2bCircuitInputs; +use blake2_rfc::blake2b::blake2b; + +pub fn benchmarking_block_sizes() -> Vec { + vec![1, 5, 10, 20, 30] +} + +pub fn sample_size() -> usize { + 30 +} + +pub fn configure_group(group: &mut BenchmarkGroup) { + group.sampling_mode(SamplingMode::Flat); + group.sample_size(sample_size()); + //group.measurement_time(Duration::from_secs(1000)); +} + +pub fn random_input_for_desired_blocks(amount_of_blocks: usize) -> Blake2bCircuitInputs { + let mut rng = rand::thread_rng(); + + let input_size = amount_of_blocks * 128; + const OUTPUT_SIZE: usize = 64; + let random_input_bytes: Vec = (0..input_size).map(|_| rng.gen_range(0..=255)).collect(); + let random_inputs: &str = &hex::encode(&random_input_bytes); + let key: &str = ""; + let output_size = OUTPUT_SIZE; + + let hash_result = run_blake2b(random_inputs, key, output_size); + + let expected_output_: Vec = hash_result.iter().map(|byte| Fq::from(*byte as u64)).collect(); + let expected_output: [Fq; OUTPUT_SIZE] = expected_output_.try_into().unwrap(); + let input_values: Vec> = + random_input_bytes.iter().map(|x| Value::known(Fq::from(*x as u64))).collect(); + let key_size = 0; + let key_values: Vec> = vec![]; + + (input_values, input_size, key_values, key_size, expected_output, OUTPUT_SIZE) +} + +fn run_blake2b(input: &str, key: &str, output_size: usize) -> Vec { + let res = blake2b(output_size, key.as_bytes(), input.as_bytes()); + res.as_bytes().into() +} diff --git a/third_party/blake2b_halo2/benches/verification.rs b/third_party/blake2b_halo2/benches/verification.rs new file mode 100644 index 000000000..27feacea8 --- /dev/null +++ b/third_party/blake2b_halo2/benches/verification.rs @@ -0,0 +1,44 @@ +use blake2b_halo2::usage_utils::circuit_runner::CircuitRunner; +use criterion::measurement::WallTime; +use criterion::{criterion_group, criterion_main, BenchmarkGroup, BenchmarkId, Criterion, Throughput}; +use midnight_curves::bls12_381::Bls12; +use midnight_proofs::poly::kzg::params::ParamsKZG; + +pub mod utils; +use utils::*; + +criterion_group!(verify, benchmark_verification); +criterion_main!(verify); + +pub fn benchmark_verification(c: &mut Criterion) { + let mut group = c.benchmark_group("verify"); + configure_group(&mut group); + + let params = ParamsKZG::::unsafe_setup(17, &mut rand::thread_rng()); + + for amount_of_blocks in benchmarking_block_sizes() { + group.throughput(Throughput::Bytes(amount_of_blocks as u64)); + + benchmark_verification_iteration(¶ms, &mut group, amount_of_blocks, "opt_recycle"); + } + group.finish() +} + +fn benchmark_verification_iteration( + params: &ParamsKZG, + group: &mut BenchmarkGroup, + amount_of_blocks: usize, + name: &str, +) { + let ci = random_input_for_desired_blocks(amount_of_blocks); + let expected_output_fields = ci.4; + + let circuit = CircuitRunner::create_circuit_for_packed_inputs(ci); + let vk = CircuitRunner::create_vk(&circuit, params); + let pk = CircuitRunner::create_pk(&circuit, vk.clone()); + let proof = CircuitRunner::create_proof(&expected_output_fields, circuit.clone(), params, &pk); + + group.bench_function(BenchmarkId::new(name, amount_of_blocks), |b| { + b.iter(|| CircuitRunner::verify(&expected_output_fields, params, pk.clone(), &proof)) + }); +} diff --git a/third_party/blake2b_halo2/benches/vk_generation.rs b/third_party/blake2b_halo2/benches/vk_generation.rs new file mode 100644 index 000000000..200262c03 --- /dev/null +++ b/third_party/blake2b_halo2/benches/vk_generation.rs @@ -0,0 +1,45 @@ +use blake2b_halo2::usage_utils::circuit_runner::CircuitRunner; +use criterion::measurement::WallTime; +use criterion::{ + criterion_group, criterion_main, BatchSize, BenchmarkGroup, BenchmarkId, Criterion, Throughput, +}; +use midnight_curves::bls12_381::Bls12; +use midnight_proofs::poly::kzg::params::ParamsKZG; + +pub mod utils; +use utils::*; + +criterion_group!(vk, benchmark_verification_key_generation); +criterion_main!(vk); + +pub fn benchmark_verification_key_generation(c: &mut Criterion) { + let mut group = c.benchmark_group("verification_key"); + configure_group(&mut group); + + let params = ParamsKZG::::unsafe_setup(17, &mut rand::thread_rng()); + + for amount_of_blocks in benchmarking_block_sizes() { + group.throughput(Throughput::Bytes(amount_of_blocks as u64)); + + benchmark_verification_key(¶ms, &mut group, amount_of_blocks, "opt_recycle"); + } + group.finish() +} + +fn benchmark_verification_key( + params: &ParamsKZG, + group: &mut BenchmarkGroup, + amount_of_blocks: usize, + name: &str, +) { + group.bench_function(BenchmarkId::new(name, amount_of_blocks), |b| { + b.iter_batched( + || { + let ci = random_input_for_desired_blocks(amount_of_blocks); + CircuitRunner::create_circuit_for_packed_inputs(ci.clone()) + }, + |circuit| CircuitRunner::create_vk(&circuit, params), + BatchSize::SmallInput, + ) + }); +} diff --git a/third_party/blake2b_halo2/examples/inputs.json b/third_party/blake2b_halo2/examples/inputs.json new file mode 100644 index 000000000..92415b6b4 --- /dev/null +++ b/third_party/blake2b_halo2/examples/inputs.json @@ -0,0 +1,5 @@ +{ + "in": "", + "key": "", + "output_size": 64 +} \ No newline at end of file diff --git a/third_party/blake2b_halo2/examples/interface.rs b/third_party/blake2b_halo2/examples/interface.rs new file mode 100644 index 000000000..ae24e9581 --- /dev/null +++ b/third_party/blake2b_halo2/examples/interface.rs @@ -0,0 +1,112 @@ +use blake2_rfc::blake2b::blake2b; +use blake2b_halo2::usage_utils::blake2b_circuit::Blake2bCircuit; +use midnight_proofs::circuit::Value; +// use midnight_proofs::dev::cost_model::{from_circuit_to_cost_model_options, CostOptions}; +use midnight_proofs::dev::MockProver; +use midnight_curves::bls12_381::Fq; +use serde::Deserialize; +use std::cmp::max; + +#[derive(Deserialize, Debug)] +struct Blake2bInput { + #[serde(rename = "in")] + input: String, + key: String, + output_size: usize, +} + +fn main() { + let workspace_root = env!("CARGO_MANIFEST_DIR"); + let file_path = format!("{}/examples/inputs.json", workspace_root); + + let file_content = std::fs::read_to_string(file_path).expect("Failed to read input file"); + let complete_input: Blake2bInput = + serde_json::from_str(&file_content).expect("Failed to parse input"); + + let input = &complete_input.input; + let key = &complete_input.key; + let output_size = complete_input.output_size; + + let input_bytes = hex::decode(input).expect("Failed decode"); + let key_bytes = hex::decode(key).expect("Failed decode"); + + let buffer_out = run_blake2b_rust(input, key, output_size); + + println!("Hash digest bytes: {:?}\n\n", buffer_out); + println!("The amount of bytes in your input is {}", input_bytes.len()); + println!("The amount of bytes in your key is {}", key_bytes.len()); + println!( + "The amount of blocks processed by the hash is {}", + amount_of_blocks(&input_bytes, &key_bytes) + ); + println!( + "The amount of rows in the circuit depends only on the amount of blocks, so two inputs \ + of different sizes but same amount of blocks will have same length in the circuit\n\n" + ); + println!("Computing the circuit and generating the proof, this could take a couple of seconds ...\n\n"); + /*let cost_options = */ + run_blake2b_halo2(input_bytes.clone(), key_bytes.clone(), buffer_out); + println!("Cost model options: "); + /*println!( + "The amount of advice rows is {} (for {} blocks of input)", + cost_options.rows_count, + amount_of_blocks(&input_bytes, &key_bytes) + ); + println!("The amount of advice columns is {}", cost_options.advice.len()); + println!("The amount of instance columns is {}", cost_options.instance.len()); + println!("The amount of fixed columns is {}", cost_options.fixed.len()); + println!("The gate degree is {}", cost_options.gate_degree); + println!("The max degree is {}", cost_options.max_degree); + println!("The table rows count is {}", cost_options.table_rows_count); + println!("The compressed rows count is {}", cost_options.compressed_rows_count);*/ +} + +fn run_blake2b_rust(input: &str, key: &str, output_size: usize) -> Vec { + let res = blake2b(output_size, key.as_bytes(), input.as_bytes()); + res.as_bytes().into() +} + +fn run_blake2b_halo2(input_bytes: Vec, key_bytes: Vec, expected_output: Vec) +/*-> CostOptions*/ +{ + // INPUT + let input_size = input_bytes.len(); + let input_values = + input_bytes.iter().map(|x| Value::known(Fq::from(*x as u64))).collect::>(); + + // OUTPUT + let output_size = expected_output.len(); + let expected_output_fields: Vec = + expected_output.iter().map(|x| Fq::from(*x as u64)).collect::>(); + + // KEY + let key_size = key_bytes.len(); + let key_values = + key_bytes.iter().map(|x| Value::known(Fq::from(*x as u64))).collect::>(); + + // TEST + let circuit = + Blake2bCircuit::::new(input_values, input_size, key_values, key_size, output_size); + + let k = compute_k(amount_of_blocks(&input_bytes, &key_bytes)); + // let options = from_circuit_to_cost_model_options(Some(k), &circuit, 1); + let prover = MockProver::run(k, &circuit, vec![expected_output_fields]).unwrap(); + prover.verify().unwrap(); + + // options +} + +fn compute_k(amount_of_blocks: usize) -> u32 { + let value = max(1 << 17, 3735_u32.saturating_mul(amount_of_blocks as u32)); + f64::from(value).log2().ceil() as u32 +} + +fn amount_of_blocks(input: &[u8], key: &[u8]) -> usize { + if key.is_empty() { + (input.len() as f64 / 128f64).ceil() as usize + } else if input.is_empty() { + 1 + } else { + input.len() / 128 + 1 + } +} diff --git a/third_party/blake2b_halo2/rust-toolchain.toml b/third_party/blake2b_halo2/rust-toolchain.toml new file mode 100644 index 000000000..b475f2f91 --- /dev/null +++ b/third_party/blake2b_halo2/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.85.0" +components = ["rustfmt", "clippy"] diff --git a/third_party/blake2b_halo2/src/base_operations/addition_mod_64.rs b/third_party/blake2b_halo2/src/base_operations/addition_mod_64.rs new file mode 100644 index 000000000..1245d861b --- /dev/null +++ b/third_party/blake2b_halo2/src/base_operations/addition_mod_64.rs @@ -0,0 +1,141 @@ +use midnight_proofs::plonk::Constraints; +use super::*; +use crate::base_operations::types::bit::AssignedBit; +use crate::base_operations::types::blake2b_word::{AssignedBlake2bWord, Blake2bWord}; +use crate::base_operations::types::row::AssignedRow; + +/// Config used to constrain addition mod 64-bits. It generates +/// a decomposed result in limbs, which will be used in one of the optimizations. +#[derive(Clone, Debug)] +pub(crate) struct AdditionMod64Config { + carry: Column, + pub(crate) q_add: Selector, + q_decompose: Selector, + q_range: Selector, +} + +impl AdditionMod64Config { + /// Creates the necessary gate for the operation to be constrained + /// The gate that will be used to check the sum of two numbers mod 2^64 + /// The gate is defined as: + /// sum mod 2 ^ 64 = full_number_result - full_number_x - full_number_y + /// + carry * (1 << 64) + /// carry = carry * (1 - carry) + /// + /// Note that the full number is range checked to be a 64-bit number because we + /// are using 8-bit limbs and the q_decompose and q_range selectors below. + pub(crate) fn configure( + meta: &mut ConstraintSystem, + full_number_u64: Column, + carry: Column, + q_decompose: Selector, + q_range: Selector, + ) -> Self { + let q_add = meta.complex_selector(); + + meta.create_gate("sum mod 2 ^ 64", |meta| { + let q_add = meta.query_selector(q_add); + let full_number_x = meta.query_advice(full_number_u64, Rotation(0)); + let full_number_y = meta.query_advice(full_number_u64, Rotation(1)); + let full_number_result = meta.query_advice(full_number_u64, Rotation(2)); + let carry = meta.query_advice(carry, Rotation(1)); + + let constraints = vec![ + q_add.clone() + * (full_number_result - full_number_x - full_number_y + + carry.clone() * (Expression::Constant(F::from_u128(1u128 << 64)))), + q_add * carry.clone() * (Expression::Constant(F::from_u128(1u128)) - carry), + ]; + + Constraints::without_selector(constraints) + }); + + Self { + carry, + q_add, + q_decompose, + q_range, + } + } + + /// This method receives two cells, copies the values of the cells to the trace and then + /// calculates the result and carry of the addition and write it in a third row. + /// + /// When one of the addition parameters (previous_cell) + /// is the last cell that was generated in the circuit, by setting the [use_last_cell_as_first_operand] + /// to [true] we can avoid copying the value of previous_cell again, and just copy the cell_to_copy. + /// This saves one row per addition. + #[allow(clippy::too_many_arguments)] + pub(crate) fn generate_addition_rows_from_cells( + &self, + region: &mut Region<'_, F>, + offset: &mut usize, + previous_cell: &AssignedBlake2bWord, + cell_to_copy: &AssignedBlake2bWord, + use_last_cell_as_first_operand: bool, + full_number_u64_column: Column, + limbs: [Column; 8], + ) -> Result<(AssignedRow, AssignedBit), Error> { + let (result_value, carry_value) = + Self::calculate_result_and_carry(previous_cell.value(), cell_to_copy.value()); + let offset_to_enable = *offset - if use_last_cell_as_first_operand { 1 } else { 0 }; + self.q_add.enable(region, offset_to_enable)?; + + if !use_last_cell_as_first_operand { + AssignedBlake2bWord::copy_advice_word( + previous_cell, + region, + full_number_u64_column, + *offset, + "Sum first operand", + )?; + *offset += 1; + } + AssignedBlake2bWord::copy_advice_word( + cell_to_copy, + region, + full_number_u64_column, + *offset, + "Sum second operand", + )?; + + let carry_cell = + AssignedBit::assign_advice_bit(region, "carry", self.carry, *offset, carry_value)?; + *offset += 1; + + self.q_decompose.enable(region, *offset)?; + self.q_range.enable(region, *offset)?; + let result_row = generate_row_from_word_value( + region, + result_value, + *offset, + full_number_u64_column, + limbs, + )?; + *offset += 1; + + Ok((result_row, carry_cell)) + } + + /// Given 2 operand values, known at proof generation time, returns the values holding the + /// result of that sum mod 2^64 and the carry value, which must be 0 or 1. Both ranges will be + /// constrained by this gate. + fn calculate_result_and_carry( + lhs: Value, + rhs: Value, + ) -> (Value, Value) { + let result_value = lhs.and_then(|l| rhs.and_then(|r| Value::known(Self::sum_mod_64(l, r)))); + let carry_value = + lhs.and_then(|l| rhs.and_then(|r| Value::known(Self::carry_mod_64(l, r)))); + (result_value, carry_value) + } + + fn sum_mod_64(a: Blake2bWord, b: Blake2bWord) -> Blake2bWord { + (((a.0 as u128 + b.0 as u128) % (1u128 << 64)) as u64).into() + } + + fn carry_mod_64(a: Blake2bWord, b: Blake2bWord) -> F { + let carry = (a.0 as u128 + b.0 as u128) / (1u128 << 64); + F::from(carry as u64) + } +} diff --git a/third_party/blake2b_halo2/src/base_operations/generic_limb_rotation.rs b/third_party/blake2b_halo2/src/base_operations/generic_limb_rotation.rs new file mode 100644 index 000000000..b1e0e6c90 --- /dev/null +++ b/third_party/blake2b_halo2/src/base_operations/generic_limb_rotation.rs @@ -0,0 +1,76 @@ +use super::*; +use crate::base_operations::types::blake2b_word::AssignedBlake2bWord; +use crate::base_operations::types::byte::AssignedByte; +use crate::base_operations::types::row::AssignedRow; +use ff::PrimeField; +use midnight_proofs::circuit::Value; + +/// This gate rotates the limbs of a number to the right and uses copy constrains to ensure that +/// the rotation is correct. It's used in our circuit to implement 16-bit, 24-bit and 32-bit rotations. +#[derive(Clone, Debug)] +pub(crate) struct LimbRotation { + q_decompose: Selector, +} + +impl LimbRotation { + pub(crate) fn configure(q_decompose: Selector) -> Self { + Self { q_decompose } + } + + /// This method receives a row of cells, and rotates the limbs to the right by the number + /// specified in the limbs_to_rotate_to_the_right parameter. It then constrains the output + /// to be the correct rotation of the input. + /// For this method to work, the input_row must be the last row of the trace at the moment + /// the method is called + pub(crate) fn generate_rotation_rows_from_input_row( + &self, + region: &mut Region<'_, F>, + offset: &mut usize, + input_row: AssignedRow, + limbs_to_rotate_to_the_right: usize, + full_number_u64_column: Column, + limbs: [Column; 8], + ) -> Result, Error> { + let result_value = + Self::right_rotation_value(input_row.full_number.value(), limbs_to_rotate_to_the_right); + + let result_cell = region + .assign_advice( + || "Full number rotation output", + full_number_u64_column, + *offset, + || result_value, + )? + .into(); + + self.q_decompose.enable(region, *offset)?; + + for i in 0..8 { + // We must subtract limb_rotations_right because if a number is expressed bitwise + // as x = l1|l2|...|l7|l8, the limbs are stored as [l8, l7, ..., l2, l1] + let top_assigned_cell = input_row.limbs[i].clone(); + let out_limb_index = (8 + i - limbs_to_rotate_to_the_right) % 8; + AssignedByte::copy_advice_byte( + region, + "Limb rotation output", + limbs[out_limb_index], + *offset, + top_assigned_cell, + )?; + } + + *offset += 1; + Ok(result_cell) + } + + /// Computes the actual value of the rotation of the number + fn right_rotation_value( + value: Value, + limbs_to_rotate: usize, + ) -> Value { + value.map(|input| { + let bits_to_rotate = limbs_to_rotate * 8; + rotate_right_field_element(input, bits_to_rotate) + }) + } +} diff --git a/third_party/blake2b_halo2/src/base_operations/mod.rs b/third_party/blake2b_halo2/src/base_operations/mod.rs new file mode 100644 index 000000000..aa3ea489e --- /dev/null +++ b/third_party/blake2b_halo2/src/base_operations/mod.rs @@ -0,0 +1,219 @@ +use midnight_proofs::plonk::Constraints; +use super::*; +use crate::base_operations::types::blake2b_word::AssignedBlake2bWord; +use crate::base_operations::types::byte::{AssignedByte, Byte}; +use crate::base_operations::types::row::AssignedRow; +use crate::base_operations::types::AssignedNative; +use types::blake2b_word::Blake2bWord; + +pub mod addition_mod_64; +pub mod negate; +pub mod xor; + +pub mod generic_limb_rotation; +pub mod rotate_63; + +/// Given a [Blake2bWord], it returns another [Blake2bWord] with the original word rotated to the +/// right by 'rotation_degree' bits. +fn rotate_right_field_element(value_to_rotate: Blake2bWord, rotation_degree: usize) -> Blake2bWord { + let value_to_rotate = value_to_rotate.0; + let rotation_degree = rotation_degree % 64; + let rotated_value = ((value_to_rotate as u128) >> rotation_degree) + | ((value_to_rotate as u128) << (64 - rotation_degree)); + (rotated_value as u64).into() +} + +/// Given an array of [AssignedNative] byte-values, it puts in the circuit a full row with those +/// bytes in the limbs and the resulting full number in the first column. +/// WARNING: this method doesn't set any constraints. That's the responsibility of the caller. +pub(crate) fn generate_row_from_assigned_bytes( + region: &mut Region<'_, F>, + bytes: &[AssignedNative; 8], + offset: usize, + full_number_u64: Column, + limbs: [Column; 8], +) -> Result, Error> { + // Compute the full number from the limbs + let full_number_cell = AssignedBlake2bWord::assign_advice_word_from_field( + region, + "full number", + full_number_u64, + offset, + compute_full_value_u64_from_bytes(bytes), + )?; + + let mut assigned_limbs = vec![]; + + // Fill the row with copies of the limbs + for (index, byte_cell) in bytes.iter().enumerate() { + let assigned_byte = AssignedByte::copy_advice_byte_from_native( + region, + "Copied input byte", + limbs[index], + offset, + byte_cell.clone(), + )?; + assigned_limbs.push(assigned_byte) + } + + Ok(AssignedRow::new(full_number_cell, assigned_limbs.try_into().unwrap())) +} + +/// Given a list of limb values, it returns the full number value that the limbs build up to. +fn compute_full_value_u64_from_bytes(bytes: &[AssignedNative; 8]) -> Value { + let mut full_number = F::ZERO; + // We process the limbs from the most significant to the least significant + for byte_cell in bytes.iter().rev() { + byte_cell.value().and_then(|v| { + full_number *= F::from(256u64); + full_number += *v; + Value::::unknown() + }); + } + Value::known(full_number) +} + +/// Given a cell with a 64-bit value, it creates a new row with the copied full number and the +/// decomposition in 8-bit limbs. +/// WARNING: this method doesn't set any constraints. That's the responsibility of the caller. +fn generate_row_from_cell( + region: &mut Region<'_, F>, + cell: &AssignedBlake2bWord, + offset: usize, + full_number_u64: Column, + limbs: [Column; 8], +) -> Result, Error> { + let value = cell.value(); + let new_cells = + generate_row_from_word_and_keep_row(region, value, offset, full_number_u64, limbs)?; + region.constrain_equal(cell.cell(), new_cells.full_number.cell())?; + Ok(new_cells) +} + +/// Given a value of 64 bits, it generates a row with the assigned cells for the full number +/// and the limbs, and returns the full number +/// WARNING: this method doesn't set any constraints. That's the responsibility of the caller. +pub(crate) fn generate_row_from_word_value( + region: &mut Region<'_, F>, + value: Value, + offset: usize, + full_number_u64: Column, + limbs: [Column; 8], +) -> Result, Error> { + generate_row_from_word_and_keep_row(region, value, offset, full_number_u64, limbs) +} + +/// Method for generating a row from a value and returning the full row. +/// Given a Value, we might want to use it as an operand in the circuit, and sometimes we need +/// to establish constraints over the result's limbs. That's why we need a way to retrieve the +/// full row that was created from that value. +/// WARNING: this method doesn't set any constraints. That's the responsibility of the caller. +pub(crate) fn generate_row_from_word_and_keep_row( + region: &mut Region<'_, F>, + value: Value, + offset: usize, + full_number_u64: Column, + limbs: [Column; 8], +) -> Result, Error> { + let limb_values: [Value; 8] = + (0..8).map(|i| get_limb_from(value, i)).collect::>().try_into().unwrap(); + create_row_with_word_and_limbs(region, value, limb_values, offset, full_number_u64, limbs) +} + +/// Given a value and a limb index, it returns the value of the limb +fn get_limb_from(value: Value, limb_number: usize) -> Value { + value.map(|v| { + let number = v.to_le_bytes()[limb_number]; + Byte(number) + }) +} + +/// Given a full number and the values of the limbs. It creates a new row with these values. +/// WARNING: this method doesn't set any constraints. That's the responsibility of the caller. +pub(crate) fn create_row_with_word_and_limbs( + region: &mut Region<'_, F>, + full_value: Value, + limb_values: [Value; 8], + offset: usize, + full_number_u64: Column, + limbs: [Column; 8], +) -> Result, Error> { + let full_number_cell = AssignedBlake2bWord::assign_advice_word( + region, + "full number", + full_number_u64, + offset, + full_value, + )?; + + let assigned_limbs: Vec> = limb_values + .iter() + .enumerate() + .map(|(i, limb)| { + AssignedByte::assign_advice_byte(region, "limb", limbs[i], offset, *limb).unwrap() + }) + .collect::>(); + + Ok(AssignedRow::new(full_number_cell, assigned_limbs.try_into().unwrap())) +} + +/// Creates a gate that constraints that a given 64-bit word decomposition is correct. +/// The equation that should hold is: +/// full_number - (sum [i=0..7] -> limbs[i] * (1 << (8*i)) ) == 0 +pub(crate) fn create_limb_decomposition_gate( + meta: &mut ConstraintSystem, + q_decompose: Selector, + full_number_u64: Column, + limbs: [Column; 8], +) { + meta.create_gate("decompose in 8 bit words", |meta| { + let q_decompose = meta.query_selector(q_decompose); + let full_number = meta.query_advice(full_number_u64, Rotation::cur()); + let limbs: Vec> = + limbs.iter().map(|column| meta.query_advice(*column, Rotation::cur())).collect(); + let constraints = vec![ + q_decompose + * (full_number + - limbs[0].clone() + - limbs[1].clone() * Expression::Constant(F::from(1 << 8)) + - limbs[2].clone() * Expression::Constant(F::from(1 << 16)) + - limbs[3].clone() * Expression::Constant(F::from(1 << 24)) + - limbs[4].clone() * Expression::Constant(F::from(1 << 32)) + - limbs[5].clone() * Expression::Constant(F::from(1 << 40)) + - limbs[6].clone() * Expression::Constant(F::from(1 << 48)) + - limbs[7].clone() * Expression::Constant(F::from(1 << 56))), + ]; + Constraints::without_selector(constraints) + }) +} + +/// Creates the necessary lookups to constrain that all the limbs in a given row are in the +/// range [0, 255]. +pub(crate) fn create_range_check_gate( + meta: &mut ConstraintSystem, + t_range: TableColumn, + q_range: Selector, + limbs: [Column; 8], +) { + meta.batched_lookup("lookup limbs", Some(q_range), |meta| { + let limb = limbs.iter().map(|limb| meta.query_advice(*limb, Rotation::cur())).collect(); + vec![(limb, t_range)] + }); +} + +/// Fills the [t_range] table with values in the range [0,255] +pub(crate) fn populate_lookup_table( + layouter: &mut impl Layouter, + t_range: TableColumn, +) -> Result<(), Error> { + const LIMB_SIZE_IN_BITS: usize = 8; + layouter.assign_table( + || format!("range {LIMB_SIZE_IN_BITS}-bit check table"), + |mut table| { + for i in 0..1 << LIMB_SIZE_IN_BITS { + table.assign_cell(|| "value", t_range, i, || Value::known(F::from(i as u64)))?; + } + Ok(()) + }, + ) +} diff --git a/third_party/blake2b_halo2/src/base_operations/negate.rs b/third_party/blake2b_halo2/src/base_operations/negate.rs new file mode 100644 index 000000000..b22e3cdee --- /dev/null +++ b/third_party/blake2b_halo2/src/base_operations/negate.rs @@ -0,0 +1,70 @@ +use midnight_proofs::plonk::Constraints; +use crate::base_operations::types::blake2b_word::AssignedBlake2bWord; +use super::*; + +/// This config handles the bitwise negation of a 64-bit number. +/// +/// This gate assumes that the input +/// will already be range checked in the circuit. This is true in the context of Blake2b usage, and +/// allows us to avoid making duplicate constraints over both input and result. +#[derive(Clone, Debug)] +pub(crate) struct NegateConfig { + q_negate: Selector, +} + +impl NegateConfig { + /// The gate that will be used to negate a number + /// The gate is defined as: + /// negate = (1 << 64) - 1 - value - not_value + pub(crate) fn configure( + meta: &mut ConstraintSystem, + full_number_u64: Column, + ) -> Self { + let q_negate = meta.complex_selector(); + + meta.create_gate("negate", |meta| { + let q_negate = meta.query_selector(q_negate); + let value = meta.query_advice(full_number_u64, Rotation(0)); + let not_value = meta.query_advice(full_number_u64, Rotation(1)); + + let constraints = vec![ + q_negate + * (Expression::Constant(F::from_u128((1u128 << 64) - 1)) - value - not_value), + ]; + Constraints::without_selector(constraints) + }); + + Self { q_negate } + } + + /// This method receives a [AssignedBlake2bWord] and a [full_number_column] where it will be + /// copied. In the same column, the result is placed in the next row. The gate constrains the + /// result. + pub(crate) fn generate_rows_from_cell( + &self, + region: &mut Region<'_, F>, + offset: &mut usize, + input: &AssignedBlake2bWord, + full_number_column: Column, + ) -> Result, Error> { + self.q_negate.enable(region, *offset)?; + AssignedBlake2bWord::copy_advice_word( + input, + region, + full_number_column, + *offset, + "Negation input", + )?; + *offset += 1; + + let result_value: Value = + input.value().map(|input| Blake2bWord(u64::MAX) - input); + + let result_cell = region + .assign_advice(|| "Negation output", full_number_column, *offset, || result_value)? + .into(); + + *offset += 1; + Ok(result_cell) + } +} diff --git a/third_party/blake2b_halo2/src/base_operations/rotate_63.rs b/third_party/blake2b_halo2/src/base_operations/rotate_63.rs new file mode 100644 index 000000000..063ecd2f5 --- /dev/null +++ b/third_party/blake2b_halo2/src/base_operations/rotate_63.rs @@ -0,0 +1,91 @@ +use midnight_proofs::plonk::Constraints; +use super::*; +use num_bigint::BigUint; +use crate::base_operations::types::blake2b_word::AssignedBlake2bWord; + +/// This config handles the 63-right-bit rotation of a 64-bit number, which is the same as the +/// 1-bit rotation to the left. +/// +/// For the gate of this config to be sound, it is necessary that the modulus of the field is +/// greater than 2^65. +/// +/// This gate assumes that the input will already be range checked in the circuit and this allows us +/// to avoid making duplicate constraints. This condition holds in the context of Blake2b usage, +/// because every time a rot63 operation appears is after a xor operation, and rot63 reuses the +/// last row from the xor, which is the result, and therefore is range checked by the xor operation. +#[derive(Clone, Debug)] +pub(crate) struct Rotate63Config { + pub q_rot63: Selector, + q_decompose: Selector, + q_range: Selector, +} + +impl Rotate63Config { + /// The gate that will be used to rotate a number 63 bits to the right + /// The gate is defined as: + /// 0 = 2 * input_full_number - output_full_number + /// * (2 * input_full_number - output_full_number - (1 << 64 - 1)) + pub(crate) fn configure( + meta: &mut ConstraintSystem, + full_number_u64: Column, + q_decompose: Selector, + q_range: Selector, + ) -> Self { + Self::enforce_modulus_size::(); + + let q_rot63 = meta.complex_selector(); + + meta.create_gate("rotate right 63", |meta| { + let q_rot63 = meta.query_selector(q_rot63); + let input_full_number = meta.query_advice(full_number_u64, Rotation(-1)); + let output_full_number = meta.query_advice(full_number_u64, Rotation(0)); + let constraints = vec![ + q_rot63 + * (Expression::Constant(F::from(2)) * input_full_number.clone() + - output_full_number.clone()) + * (Expression::Constant(F::from(2)) * input_full_number + - output_full_number + - Expression::Constant(F::from(((1u128 << 64) - 1) as u64))), + ]; + Constraints::without_selector(constraints) + }); + + Self { + q_rot63, + q_decompose, + q_range, + } + } + + /// This method receives a [AssignedBlake2bWord] and a [full_number_u64] column where it will be + /// copied. In the same column, the result is placed in the next row. The gate constrains the + /// result. + pub(crate) fn generate_64_bit_rotation_from_cells( + &self, + region: &mut Region<'_, F>, + offset: &mut usize, + input: &AssignedBlake2bWord, + full_number_u64: Column, + limbs: [Column; 8], + ) -> Result, Error> { + self.q_rot63.enable(region, *offset)?; + let result_value = input.value().map(|input| rotate_right_field_element(input, 63)); + + self.q_decompose.enable(region, *offset)?; + self.q_range.enable(region, *offset)?; + let result_row = + generate_row_from_word_value(region, result_value, *offset, full_number_u64, limbs)?; + *offset += 1; + Ok(result_row.full_number) + } + + /// Enforces the field's modulus to be greater than 2^65. This is necessary to preserve the + /// soundness of a circuit that uses this operation. + pub(crate) fn enforce_modulus_size() { + let modulus_bytes: Vec = hex::decode(F::MODULUS.trim_start_matches("0x")) + .expect("Modulus is not a valid hex number"); + let modulus = BigUint::from_bytes_be(&modulus_bytes); + let two_pow_65 = BigUint::from(1u128 << 65); + assert!(modulus > two_pow_65, "Field modulus must be greater than 2^65"); + } +} diff --git a/third_party/blake2b_halo2/src/base_operations/xor.rs b/third_party/blake2b_halo2/src/base_operations/xor.rs new file mode 100644 index 000000000..c408352ad --- /dev/null +++ b/third_party/blake2b_halo2/src/base_operations/xor.rs @@ -0,0 +1,201 @@ +use super::*; +use crate::base_operations::types::blake2b_word::AssignedBlake2bWord; +use crate::base_operations::types::byte::Byte; +use crate::base_operations::types::row::AssignedRow; + +/// This config handles the xor operation in the trace. Requires a representation in 8-bit limbs +/// because it uses a lookup table like this one: +/// +/// | lhs | rhs | lhs xor rhs | +/// | 0 | 0 | 0 | +/// | 0 | 1 | 1 | +/// ... +/// | 255 | 255 | 0 | +/// +/// The table has 2^8 * 2^8 = 2^16 rows, since we need to check all the possible +/// combinations of 8-bit numbers. +/// Then, with the help of the Decompose8Config, the final representation in the trace will be: +/// +/// | full_number_lhs | limb_0_lhs | limb_1_lhs | ... | limb_7_lhs | +/// | full_number_rhs | limb_0_rhs | limb_1_rhs | ... | limb_7_rhs | +/// | full_number_result | limb_0_result | limb_1_result | ... | limb_7_result | +#[derive(Clone, Debug)] +pub(crate) struct XorConfig { + /// Lookup table columns + t_xor_left: TableColumn, + t_xor_right: TableColumn, + t_xor_out: TableColumn, + + /// Selector for the xor gate + pub q_xor: Selector, + + /// Involved columns + full_number_u64: Column, + limbs: [Column; 8], + + /// Selector for the decomposition + q_decompose: Selector, +} + +impl XorConfig { + /// Method that populates the lookup table. Must be called only once in the user circuit. + pub(crate) fn populate_xor_lookup_table( + &self, + layouter: &mut impl Layouter, + ) -> Result<(), Error> { + layouter.assign_table( + || "xor check table", + |mut table| { + for left in 0..256 { + for right in 0..256 { + let index = left * 256 + right; + let result = left ^ right; + table.assign_cell( + || "left_value", + self.t_xor_left, + index, + || Value::known(F::from(left as u64)), + )?; + table.assign_cell( + || "right_value", + self.t_xor_right, + index, + || Value::known(F::from(right as u64)), + )?; + table.assign_cell( + || "out_value", + self.t_xor_out, + index, + || Value::known(F::from(result as u64)), + )?; + } + } + Ok(()) + }, + )?; + Ok(()) + } + + /// This method generates the xor rows in the trace. Copying both operands into new rows on the + /// trace and then performing the xor operation on the row limbs. Each limb of the result is + /// looked up in a table to check that it is the xor result of the corresponding limbs of the + /// operands + pub(crate) fn generate_xor_rows_from_cells( + &self, + region: &mut Region<'_, F>, + offset: &mut usize, + lhs: &AssignedBlake2bWord, + rhs: &AssignedBlake2bWord, + ) -> Result, Error> { + self.q_xor.enable(region, *offset)?; + + // We only enable decomposition because the range-checks are performed by the lookups of the gate + self.q_decompose.enable(region, *offset)?; + let first_operand_row = + generate_row_from_cell(region, rhs, *offset, self.full_number_u64, self.limbs)?; + *offset += 1; + + // We only enable decomposition because the range-checks are performed by the lookups of the gate + self.q_decompose.enable(region, *offset)?; + let second_operand_row = + generate_row_from_cell(region, lhs, *offset, self.full_number_u64, self.limbs)?; + *offset += 1; + + self.generate_xor_result_row(region, offset, &first_operand_row, &second_operand_row) + } + + /// This is similar to generate_xor_rows_from_cells but it reuses the first operand of the + /// operation Note that this method will work only if first_operand_row is the immediate + /// previous row in the trace. + pub(crate) fn generate_xor_rows_reusing_first_operand( + &self, + region: &mut Region<'_, F>, + offset: &mut usize, + first_operand_row: &AssignedRow, + second_operand: &AssignedBlake2bWord, + ) -> Result, Error> { + // Since the first row is being reused, the selector must be enabled for offset - 1 + self.q_xor.enable(region, *offset - 1)?; + + // We only enable decomposition because the range-checks are performed by the lookups of the gate + self.q_decompose.enable(region, *offset)?; + let second_operand_row = generate_row_from_cell( + region, + second_operand, + *offset, + self.full_number_u64, + self.limbs, + )?; + *offset += 1; + + self.generate_xor_result_row(region, offset, first_operand_row, &second_operand_row) + } + + /// This method uses [create_row_with_word_and_limbs] which is a method that doesn't range + /// check the limbs. This is on purpose, because those limbs will be range-checked by this + /// gate when doing the lookups. + fn generate_xor_result_row( + &self, + region: &mut Region<'_, F>, + offset: &mut usize, + first_operand_row: &AssignedRow, + second_operand_row: &AssignedRow, + ) -> Result, Error> { + let mut result_limb_values: Vec> = Vec::with_capacity(8); + for i in 0..8 { + let left = first_operand_row.limbs[i].clone(); + let right = second_operand_row.limbs[i].clone(); + let result_value = left.value().zip(right.value()).map(|(v0, v1)| v0 ^ v1); + result_limb_values.push(result_value) + } + let result_value = first_operand_row + .full_number + .value() + .zip(second_operand_row.full_number.value()) + .map(|(v0, v1)| v0 ^ v1); + + // We only enable decomposition because the range-checks are performed by the lookups of the gate + self.q_decompose.enable(region, *offset)?; + let result_row = create_row_with_word_and_limbs( + region, + result_value, + result_limb_values.try_into().unwrap(), + *offset, + self.full_number_u64, + self.limbs, + )?; + *offset += 1; + Ok(result_row) + } + + pub(crate) fn configure( + meta: &mut ConstraintSystem, + limbs_8_bits: [Column; 8], + full_number_u64: Column, + limbs: [Column; 8], + q_decompose: Selector, + ) -> Self { + let q_xor = meta.complex_selector(); + let t_xor_left = meta.lookup_table_column(); + let t_xor_right = meta.lookup_table_column(); + let t_xor_out = meta.lookup_table_column(); + + // We need to perform a lookup for each limb + meta.batched_lookup("xor lookup limbs_8_bits", Some(q_xor), |meta| { + let left = limbs_8_bits.iter().map(|limb_col| meta.query_advice(*limb_col, Rotation(0))).collect(); + let right = limbs_8_bits.iter().map(|limb_col| meta.query_advice(*limb_col, Rotation(1))).collect(); + let out = limbs_8_bits.iter().map(|limb_col| meta.query_advice(*limb_col, Rotation(2))).collect(); + vec![(left, t_xor_left), (right, t_xor_right), (out, t_xor_out)] + }); + + Self { + t_xor_left, + t_xor_right, + t_xor_out, + q_xor, + full_number_u64, + limbs, + q_decompose, + } + } +} diff --git a/third_party/blake2b_halo2/src/blake2b/blake2b_chip.rs b/third_party/blake2b_halo2/src/blake2b/blake2b_chip.rs new file mode 100644 index 000000000..4969e2d49 --- /dev/null +++ b/third_party/blake2b_halo2/src/blake2b/blake2b_chip.rs @@ -0,0 +1,733 @@ +use std::marker::PhantomData; + +use crate::base_operations::addition_mod_64::AdditionMod64Config; +use crate::base_operations::generic_limb_rotation::LimbRotation; +use crate::base_operations::negate::NegateConfig; +use crate::base_operations::rotate_63::Rotate63Config; +use crate::types::blake2b_word::AssignedBlake2bWord; +use crate::types::byte::AssignedByte; +use crate::types::row::AssignedRow; +use crate::types::AssignedNative; +use crate::base_operations::xor::XorConfig; +use crate::base_operations::{ + create_limb_decomposition_gate, create_range_check_gate, generate_row_from_assigned_bytes, + populate_lookup_table, +}; +use crate::blake2b::blake2b_instructions::{Blake2bInstructions, ConstantCells}; +use crate::blake2b::utils::{ + compute_processed_bytes_count_value_for_iteration, constrain_padding_cells_to_equal_zero, + enforce_input_sizes, full_number_of_each_state_row, get_total_blocks_count, + zeros_to_pad_in_current_block, ABCD, BLAKE2B_BLOCK_SIZE, IV_CONSTANTS, SIGMA, +}; +use crate::blake2b::NB_BLAKE2B_ADVICE_COLS; +use ff::{Field, PrimeField}; +use midnight_proofs::circuit::{Chip, Layouter, Region}; +use midnight_proofs::plonk::{Advice, Column, ConstraintSystem, Error, Fixed, Selector, TableColumn}; + +/// Selectors and columns for the blake2b chip implementation. +#[derive(Clone, Debug)] +pub struct Blake2bConfig { + /// Base oprerations configs + addition_config: AdditionMod64Config, + generic_limb_rotation_config: LimbRotation, + rotate_63_config: Rotate63Config, + xor_config: XorConfig, + negate_config: NegateConfig, + /// Advice columns + full_number_u64: Column, + /// Columns for the blake2b limbs. + pub limbs: [Column; 8], + /// Decomposition selectors + q_range: Selector, + q_decompose: Selector, + t_range: TableColumn, +} + +/// This is the main chip for the Blake2b hash function. It is responsible for the entire hash computation. +/// It contains all the necessary chips and some extra columns. +/// +/// This implementation uses addition with 8 limbs and computes xor with a table that precomputes +/// all the possible 8-bit operands. Since all operations have operands with 8-bit decompositions, +/// we can recycle some rows per iteration of the algorithm for every operation. +#[derive(Clone, Debug)] +pub struct Blake2bChip { + config: Blake2bConfig, + _marker: PhantomData, +} + +impl Chip for Blake2bChip { + type Config = Blake2bConfig; + type Loaded = (); + + fn config(&self) -> &Self::Config { + &self.config + } + + fn loaded(&self) -> &Self::Loaded { + &() + } +} + +impl Blake2bInstructions for Blake2bChip { + /// This optimization uses 2 tables: + /// * A lookup table for range-checks of 8 bits: [0, 255] + /// * A lookup table consisting of 3 columns that pre-computes the xor operation of 16 bits. + fn populate_lookup_tables(&self, layouter: &mut impl Layouter) -> Result<(), Error> { + self.populate_lookup_table_8(layouter)?; + self.populate_xor_lookup_table(layouter) + } + + /// Here the constants that will be used throughout the algorithm are assigned in some storage + /// cells at the begining of the trace. + fn assign_constant_advice_cells( + &self, + output_size: usize, + key_size: usize, + region: &mut Region<'_, F>, + advice_offset: &mut usize, + ) -> Result, Error> { + let iv_constant_cells: [AssignedBlake2bWord; 8] = + self.assign_iv_constants_to_fixed_cells(region, advice_offset)?; + + let zero_constant = region.assign_advice_from_constant( + || "zero", + self.config.limbs[0], + *advice_offset, + F::from(0), + )?; + + let iv_constant_0 = IV_CONSTANTS[0]; + let out_len = output_size as u64; + const INIT_CONST_STATE_0: u64 = 0x01010000u64; + let key_size_shifted = (key_size as u64) << 8; + // state[0] = state[0] ^ 0x01010000 ^ (key.len() << 8) as u64 ^ outlen as u64; + let initial_state_index_0 = iv_constant_0 ^ INIT_CONST_STATE_0 ^ key_size_shifted ^ out_len; + + let initial_state_0 = self.assign_limb_constant_u64( + region, + advice_offset, + "initial state index 0", + initial_state_index_0, + 1, + )?; + + *advice_offset += 1; + + Ok((iv_constant_cells, initial_state_0, zero_constant)) + } + + /// The initial state is known at circuit building time because it depends on fixed constants, + /// key size and output size. + fn compute_initial_state( + &self, + iv_constant_cells: &[AssignedBlake2bWord; 8], + initial_state_0: AssignedBlake2bWord, + ) -> Result<[AssignedBlake2bWord; 8], Error> { + let mut global_state = iv_constant_cells.clone(); + global_state[0] = initial_state_0; + Ok(global_state) + } + + #[allow(clippy::too_many_arguments)] + fn perform_blake2b_iterations( + &self, + region: &mut Region<'_, F>, + offset: &mut usize, + input: &[AssignedNative], + key: &[AssignedNative], + iv_constants: &[AssignedBlake2bWord; 8], + global_state: &mut [AssignedBlake2bWord; 8], + zero_constant_cell: AssignedNative, + ) -> Result<[AssignedByte; 64], Error> { + let input_size = input.len(); + let is_key_empty = key.is_empty(); + let is_input_empty = input_size == 0; + + let input_blocks = input_size.div_ceil(BLAKE2B_BLOCK_SIZE); + let total_blocks = get_total_blocks_count(input_blocks, is_input_empty, is_key_empty); + let last_input_block_index = if is_input_empty { 0 } else { input_blocks - 1 }; + + // Main loop + (0..total_blocks) + .map(|i| { + let is_last_block = i == total_blocks - 1; + let is_key_block = !is_key_empty && i == 0; + + // This is an intermediate value in the Blake2b algorithm. It represents the amount + // of bytes processed so far. + let processed_bytes_count = compute_processed_bytes_count_value_for_iteration( + i, + is_last_block, + input_size, + is_key_empty, + ); + + let amount_of_zeros_to_pad = + zeros_to_pad_in_current_block(key, input_size, is_last_block, is_key_block); + + let current_block_values = Self::build_values_for_current_block( + input, + key, + i, + last_input_block_index, + is_key_empty, + is_last_block, + is_key_block, + zero_constant_cell.clone(), + ); + + let current_block_rows = self.block_words_from_bytes( + region, + offset, + current_block_values.try_into().unwrap(), + )?; + + constrain_padding_cells_to_equal_zero( + region, + amount_of_zeros_to_pad, + ¤t_block_rows, + &zero_constant_cell, + )?; + + let current_block_cells = full_number_of_each_state_row(current_block_rows); + + self.compress( + region, + offset, + iv_constants, + global_state, + current_block_cells, + processed_bytes_count, + is_last_block, + ) + }) + .last() + // Note: `input_blocks` can only be 0 if `is_input_empty` is true, so `total_blocks` is greater or equal than 1. Therefore, this `unwrap` must succeeds. + .expect("unexpected empty sequence of blake2b blocks") + } + + fn compress( + &self, + region: &mut Region<'_, F>, + row_offset: &mut usize, + iv_constants: &[AssignedBlake2bWord; 8], + global_state: &mut [AssignedBlake2bWord; 8], + current_block: [AssignedBlake2bWord; 16], + processed_bytes_count: u64, + is_last_block: bool, + ) -> Result<[AssignedByte; 64], Error> { + let mut state_vector: Vec> = Vec::new(); + state_vector.extend_from_slice(global_state); + state_vector.extend_from_slice(iv_constants); + + let mut state: [AssignedBlake2bWord; 16] = state_vector.try_into().unwrap(); + + // accumulative_state[12] ^= processed_bytes_count + // Since accumulative_state[12] is allways IV_CONSTANTS[4] at this point in execution + // and processed_bytes_count is public for both parties, the xor between both values + // is also a constant. + let new_state_12 = processed_bytes_count ^ IV_CONSTANTS[4]; + state[12] = AssignedBlake2bWord::assign_fixed_word( + region, + "New state[12]", + self.config.full_number_u64, + *row_offset, + new_state_12.into(), + )?; + *row_offset += 1; + + if is_last_block { + state[14] = self.not(&state[14], region, row_offset)?; + } + + // Main loop + for i in 0..12 { + for j in 0..8 { + self.mix( + [ABCD[j][0], ABCD[j][1], ABCD[j][2], ABCD[j][3]], + current_block[SIGMA[i][2 * j]].clone(), + current_block[SIGMA[i][2 * j + 1]].clone(), + &mut state, + region, + row_offset, + )?; + } + } + + let mut global_state_bytes: Vec> = Vec::new(); + for i in 0..8 { + global_state[i] = + self.xor(&global_state[i], &state[i], region, row_offset)?.full_number; + let row = self.xor(&global_state[i], &state[i + 8], region, row_offset)?; + let mut row_limbs: Vec<_> = row.limbs.into(); + global_state_bytes.append(&mut row_limbs); + global_state[i] = row.full_number; + } + let global_state_bytes_array = global_state_bytes.try_into().unwrap(); + Ok(global_state_bytes_array) + } + + fn mix( + &self, + state_indexes: [usize; 4], + x: AssignedBlake2bWord, + y: AssignedBlake2bWord, + state: &mut [AssignedBlake2bWord; 16], + region: &mut Region<'_, F>, + offset: &mut usize, + ) -> Result<(), Error> { + let v_a = &state[state_indexes[0]]; + let v_b = &state[state_indexes[1]]; + let v_c = &state[state_indexes[2]]; + let v_d = &state[state_indexes[3]]; + + // v[a] = ((v[a] as u128 + v[b] as u128 + x as u128) % (1 << 64)) as u64; + let a_plus_b = self.add(v_a, v_b, region, offset)?; + let a = self.add_copying_one_parameter(&a_plus_b.full_number, &x, region, offset)?; + + // v[d] = rotr_64(v[d] ^ v[a], 32); + let d_xor_a = self.xor_copying_one_parameter(&a, v_d, region, offset)?; + let d = self.rotate_right_32(d_xor_a, region, offset)?; + + // v[c] = ((v[c] as u128 + v[d] as u128) % (1 << 64)) as u64; + let c = self.add_copying_one_parameter(&d, v_c, region, offset)?; + + // v[b] = rotr_64(v[b] ^ v[c], 24); + let b_xor_c = self.xor_copying_one_parameter(&c, v_b, region, offset)?; + let b = self.rotate_right_24(b_xor_c, region, offset)?; + + // v[a] = ((v[a] as u128 + v[b] as u128 + y as u128) % (1 << 64)) as u64; + let a_plus_b = self.add_copying_one_parameter(&b, &a.full_number, region, offset)?; + let a = self.add_copying_one_parameter(&a_plus_b.full_number, &y, region, offset)?; + + // v[d] = rotr_64(v[d] ^ v[a], 16); + let d_xor_a = self.xor_copying_one_parameter(&a, &d, region, offset)?; + let d = self.rotate_right_16(d_xor_a, region, offset)?; + + // v[c] = ((v[c] as u128 + v[d] as u128) % (1 << 64)) as u64; + let c = self.add_copying_one_parameter(&d, &c.full_number, region, offset)?; + + // v[b] = rotr_64(v[b] ^ v[c], 63); + let b_xor_c = self.xor_copying_one_parameter(&c, &b, region, offset)?; + let b = self.rotate_right_63(b_xor_c.full_number, region, offset)?; + + state[state_indexes[0]] = a.full_number; + state[state_indexes[1]] = b; + state[state_indexes[2]] = c.full_number; + state[state_indexes[3]] = d; + + Ok(()) + } +} + +impl Blake2bChip { + /// Generation of a fresh circuit from a configuration. + pub fn new(config: &Blake2bConfig) -> Self { + Self { + config: config.clone(), + _marker: PhantomData, + } + } + + /// Configuration of the circuit, this includes initialization of all the necessary configs. + /// It should be called in the configuration of the user circuit before instantiating the + /// Blake2b gadget. + /// + /// Note: following the convention in midnight-circuits, this function enables equality on all + /// necessary columns, i.e., it should not be done manually before calling this function. + pub fn configure( + meta: &mut ConstraintSystem, + constants: Column, + full_number_u64: Column, + limbs: [Column; NB_BLAKE2B_ADVICE_COLS - 1], + ) -> >::Config { + // Enabling column properties. + meta.enable_constant(constants); + meta.enable_equality(full_number_u64); + for limb in limbs { + meta.enable_equality(limb); + } + + // Gate that checks if the 8-bit limb decomposition is correct + let q_decompose = meta.complex_selector(); + create_limb_decomposition_gate(meta, q_decompose, full_number_u64, limbs); + + // Range-check lookups + let q_range = meta.complex_selector(); + let t_range = meta.lookup_table_column(); + create_range_check_gate(meta, t_range, q_range, limbs); + + // Config that is the same for every optimization + let rotate_63_config = + Rotate63Config::configure(meta, full_number_u64, q_decompose, q_range); + let negate_config = NegateConfig::configure(meta, full_number_u64); + let generic_limb_rotation_config = LimbRotation::configure(q_decompose); + + // Config that is optimization-specific + // For the carry column we'll reuse the first limb column for optimization reasons + let addition_config = + AdditionMod64Config::configure(meta, full_number_u64, limbs[0], q_decompose, q_range); + let xor_config = XorConfig::configure(meta, limbs, full_number_u64, limbs, q_decompose); + + Blake2bConfig { + addition_config, + generic_limb_rotation_config, + rotate_63_config, + xor_config, + negate_config, + full_number_u64, + limbs, + q_range, + q_decompose, + t_range, + } + } + + /// Loading the tables used in the chip. + pub fn load(&self, layouter: &mut impl Layouter) -> Result<(), Error> { + self.populate_lookup_tables(layouter) + } +} + +impl Blake2bChip { + /// Blake2b uses a fixed initialization vector (iv). This method assigns those + /// fixed values to advice cells. The cells used are the 8 limbs in the very first row of the + /// trace. This is implementation specific. + fn assign_iv_constants_to_fixed_cells( + &self, + region: &mut Region<'_, F>, + offset: &mut usize, + ) -> Result<[AssignedBlake2bWord; 8], Error> { + let ret: [AssignedBlake2bWord; 8] = IV_CONSTANTS + .iter() + .enumerate() + .map(|(index, constant)| { + self.assign_limb_constant_u64(region, offset, "iv constants", *constant, index) + .unwrap() + }) + .collect::>>() + .try_into() + .unwrap(); + *offset += 1; + Ok(ret) + } + + /// Bitwise negation operation. This is used only once in the circuit, at the beginning of the + /// last compress iteration. It's implemented through a [NegateConfig] which establishes all the + /// necessary restrictions. + fn not( + &self, + input_cell: &AssignedBlake2bWord, + region: &mut Region<'_, F>, + offset: &mut usize, + ) -> Result, Error> { + self.config.negate_config.generate_rows_from_cell( + region, + offset, + input_cell, + self.config.full_number_u64, + ) + } + + /// Bitwise xor operation. It's performed over two assigned blake2b words. Is one of the most + /// used operations in the Blake2b function and implemented through a [XorConfig] which + /// creates all the necessary lookups. + fn xor( + &self, + lhs: &AssignedBlake2bWord, + rhs: &AssignedBlake2bWord, + region: &mut Region<'_, F>, + offset: &mut usize, + ) -> Result, Error> { + self.config.xor_config.generate_xor_rows_from_cells(region, offset, lhs, rhs) + } + + /// Addition operation. It's performed over two assigned blake2b words. Is one of the most + /// used operations in the Blake2b function and implemented through a [AdditionMod64Config] + /// which creates all the necessary lookups. + fn add( + &self, + lhs: &AssignedBlake2bWord, + rhs: &AssignedBlake2bWord, + region: &mut Region<'_, F>, + offset: &mut usize, + ) -> Result, Error> { + let addition_row = self + .config + .addition_config + .generate_addition_rows_from_cells( + region, + offset, + lhs, + rhs, + false, + self.config.full_number_u64, + self.config.limbs, + )? + .0; + Ok(addition_row) + } + + /// Bitwise rotation mod 64 bits. 63 bits to the right. Internally uses a [Rotate63Config] and + /// only receives the full number as input because it doesn't need the limbs to establish the + /// necessary restrictions. + fn rotate_right_63( + &self, + input: AssignedBlake2bWord, + region: &mut Region<'_, F>, + offset: &mut usize, + ) -> Result, Error> { + self.config.rotate_63_config.generate_64_bit_rotation_from_cells( + region, + offset, + &input, + self.config.full_number_u64, + self.config.limbs, + ) + } + + /// Bitwise rotation mod 64 bits. 16 bits to the right. Internally uses the [LimbRotation] gate + /// and receives an [AssignedRow] as input because it needs the limbs to establish the + /// necessary restrictions. It only returns the full number, not the resulting row. + fn rotate_right_16( + &self, + input_row: AssignedRow, + region: &mut Region<'_, F>, + offset: &mut usize, + ) -> Result, Error> { + self.config.generic_limb_rotation_config.generate_rotation_rows_from_input_row( + region, + offset, + input_row, + 2, + self.config.full_number_u64, + self.config.limbs, + ) + } + + /// Bitwise rotation mod 64 bits. 24 bits to the right. Internally uses the [LimbRotation] gate + /// and receives an [AssignedRow] as input because it needs the limbs to establish the + /// necessary restrictions. It only returns the full number, not the resulting row. + fn rotate_right_24( + &self, + input_row: AssignedRow, + region: &mut Region<'_, F>, + offset: &mut usize, + ) -> Result, Error> { + self.config.generic_limb_rotation_config.generate_rotation_rows_from_input_row( + region, + offset, + input_row, + 3, + self.config.full_number_u64, + self.config.limbs, + ) + } + + /// Bitwise rotation mod 64 bits. 32 bits to the right. Internally uses the [LimbRotation] gate + /// and receives an [AssignedRow] as input because it needs the limbs to establish the + /// necessary restrictions. It only returns the full number, not the resulting row. + fn rotate_right_32( + &self, + input_row: AssignedRow, + region: &mut Region<'_, F>, + offset: &mut usize, + ) -> Result, Error> { + self.config.generic_limb_rotation_config.generate_rotation_rows_from_input_row( + region, + offset, + input_row, + 4, + self.config.full_number_u64, + self.config.limbs, + ) + } + + /// This method performs a regular [xor] operation with the difference that it returns the whole + /// row in the trace, instead of just the cell holding the full value. This allows an optimization + /// where the next operation (which is a rotation) can just read the limbs directly and apply + /// the limb rotation without copying them. + /// This method reuse the first operand of the operation, so it doesn't need to copy it. + /// That's why it receives a [AssignedRow] as input, to let us reuse the limbs, which we need + /// to perform the XOR operation + fn xor_copying_one_parameter( + &self, + previous_operand: &AssignedRow, + cell_to_copy: &AssignedBlake2bWord, + region: &mut Region<'_, F>, + offset: &mut usize, + ) -> Result, Error> { + self.config.xor_config.generate_xor_rows_reusing_first_operand( + region, + offset, + previous_operand, + cell_to_copy, + ) + } + + /// This method behaves like [add], with the difference that it takes advantage of the fact that + /// the last row in the circuit is one of the operands of the addition, so it only needs to copy + /// one parameter because the other is already on the trace. + fn add_copying_one_parameter( + &self, + previous_cell: &AssignedBlake2bWord, + cell_to_copy: &AssignedBlake2bWord, + region: &mut Region<'_, F>, + offset: &mut usize, + ) -> Result, Error> { + Ok(self + .config + .addition_config + .generate_addition_rows_from_cells( + region, + offset, + previous_cell, + cell_to_copy, + true, // Uses the optimization + self.config.full_number_u64, + self.config.limbs, + )? + .0) + } + + /// Fills the 8-bit range-check lookup table + fn populate_lookup_table_8(&self, layouter: &mut impl Layouter) -> Result<(), Error> { + populate_lookup_table(layouter, self.config.t_range) + } + + /// The xor lookup table is created by the [XorConfig], since it establishes the lookups over it. + fn populate_xor_lookup_table(&self, layouter: &mut impl Layouter) -> Result<(), Error> { + self.config.xor_config.populate_xor_lookup_table(layouter) + } + + /// Given an array of [AssignedNative] byte-values, it puts in the circuit a full row with those + /// bytes in the limbs and the resulting full number in the first column. The resulting values + /// are range-checked by the circuit. + fn new_row_from_assigned_bytes( + &self, + bytes: &[AssignedNative; 8], + region: &mut Region<'_, F>, + offset: &mut usize, + ) -> Result, Error> { + self.config.q_decompose.enable(region, *offset)?; + self.config.q_range.enable(region, *offset)?; + let ret = generate_row_from_assigned_bytes( + region, + bytes, + *offset, + self.config.full_number_u64, + self.config.limbs, + ); + *offset += 1; + ret + } + + /// This method is used when building the block words from the input bytes. It receives a list + /// of 128 [AssignedNative] bytes that still haven't been range-checked and returns a list of + /// 16 [AssignedRow] putted in the trace, range-checked and ready for use in the algorithm. + fn block_words_from_bytes( + &self, + region: &mut Region<'_, F>, + offset: &mut usize, + block: [AssignedNative; 128], + ) -> Result<[AssignedRow; 16], Error> { + let mut current_block_rows_vector: Vec> = Vec::new(); + for i in 0..16 { + let bytes: &[AssignedNative; 8] = block[i * 8..(i + 1) * 8].try_into().unwrap(); + let current_row_cells = self.new_row_from_assigned_bytes(bytes, region, offset)?; + current_block_rows_vector.push(current_row_cells); + } + let current_block_rows = current_block_rows_vector.try_into().unwrap(); + Ok(current_block_rows) + } + + /// Computes the values of the current block in the blake2b algorithm, based on the input and + /// the block number we're on, among other relevant data. + #[allow(clippy::too_many_arguments)] + fn build_values_for_current_block( + input: &[AssignedNative], + key: &[AssignedNative], + block_number: usize, + last_input_block_index: usize, + is_key_empty: bool, + is_last_block: bool, + is_key_block: bool, + zero_constant_cell: AssignedNative, + ) -> Vec> { + if is_last_block && !is_key_block { + let mut result = input[last_input_block_index * BLAKE2B_BLOCK_SIZE..].to_vec(); + result.resize(128, zero_constant_cell); + result + } else if is_key_block { + let mut result = key.to_vec(); + result.resize(128, zero_constant_cell); + result + } else { + let current_input_block_index = + if is_key_empty { block_number } else { block_number - 1 }; + input[current_input_block_index * BLAKE2B_BLOCK_SIZE + ..(current_input_block_index + 1) * BLAKE2B_BLOCK_SIZE] + .to_vec() + } + } + + /// Assigns an u64 constant to trace[row_offset][limbs[limb_index]] cell. + fn assign_limb_constant_u64( + &self, + region: &mut Region<'_, F>, + row_offset: &usize, + description: &str, + constant: u64, + limb_index: usize, + ) -> Result, Error> { + AssignedBlake2bWord::assign_fixed_word( + region, + description, + self.config.limbs[limb_index], + *row_offset, + constant.into(), + ) + } +} + +impl Blake2bChip { + /// Main method of the chip. The 'input' and 'key' cells should be filled with byte values. + pub fn hash( + &self, + layouter: &mut impl Layouter, + input: &[AssignedNative], + key: &[AssignedNative], + output_size: usize, + ) -> Result<[AssignedByte; 64], Error> { + enforce_input_sizes(output_size, key.len()); + // All the computation is performed inside a single region + layouter.assign_region( + || "single region", + |mut region| { + // Initialize in 0 the offset for the advice cells in the region + let mut advice_offset: usize = 0; + + let (iv_constant_cells, initial_state_0, zero_constant) = self + .assign_constant_advice_cells( + output_size, + key.len(), + &mut region, + &mut advice_offset, + )?; + + let mut initial_global_state = + self.compute_initial_state(&iv_constant_cells, initial_state_0)?; + + self.perform_blake2b_iterations( + &mut region, + &mut advice_offset, + input, + key, + &iv_constant_cells, + &mut initial_global_state, + zero_constant, + ) + }, + ) + } +} diff --git a/third_party/blake2b_halo2/src/blake2b/blake2b_instructions.rs b/third_party/blake2b_halo2/src/blake2b/blake2b_instructions.rs new file mode 100644 index 000000000..e5ea95dcc --- /dev/null +++ b/third_party/blake2b_halo2/src/blake2b/blake2b_instructions.rs @@ -0,0 +1,84 @@ +use crate::types::blake2b_word::AssignedBlake2bWord; +use crate::types::byte::AssignedByte; +use crate::types::AssignedNative; +use ff::PrimeField; +use midnight_proofs::circuit::{Layouter, Region}; +use midnight_proofs::plonk::Error; + +pub(crate) type ConstantCells = + ([AssignedBlake2bWord; 8], AssignedBlake2bWord, AssignedNative); + +/// This is the trait that groups the Blake2b implementation chips. Every Blake2b chip +/// should implement this trait. +pub trait Blake2bInstructions: Clone { + /// Populate all lookup tables needed for the chip + fn populate_lookup_tables(&self, layouter: &mut impl Layouter) -> Result<(), Error>; + + /// Assign initializations constants at the beginning. These constants are the initialization + /// vector (IV) constants, the zero constant and a constant computed from the key and output + /// lengths that is used for the initial state of the rounds. + fn assign_constant_advice_cells( + &self, + output_size: usize, + key_size: usize, + region: &mut Region<'_, F>, + advice_offset: &mut usize, + ) -> Result, Error>; + + /// Computes the initial global state of Blake2b. It only depends on the key size and the + /// output size, which are values known at circuit building time. + fn compute_initial_state( + &self, + iv_constant_cells: &[AssignedBlake2bWord; 8], + initial_state_0: AssignedBlake2bWord, + ) -> Result<[AssignedBlake2bWord; 8], Error>; + + /// Here occurs the top loop of the hash function. It iterates for each block of the input and + /// key, compressing the block and updating the global state. + /// The global state corresponds to 8 cells containing 64-bit numbers, which are updated when + /// some of those words change. A change in a state value is represented by changing the cell + /// that represent that particular word in the state. + /// The return bytes of this function are the digest of the Blake2b computation. + #[allow(clippy::too_many_arguments)] + fn perform_blake2b_iterations( + &self, + region: &mut Region<'_, F>, + advice_offset: &mut usize, + input: &[AssignedNative], + key: &[AssignedNative], + iv_constants: &[AssignedBlake2bWord; 8], + global_state: &mut [AssignedBlake2bWord; 8], + zero_constant_cell: AssignedNative, + ) -> Result<[AssignedByte; 64], Error>; + + /// This method computes a compression round of Blake2b. The global state is update through + /// consecutive calls of this method. If the algorithm is in its last round, the is_last_block + /// parameter should be set to true. + #[allow(clippy::too_many_arguments)] + fn compress( + &self, + region: &mut Region<'_, F>, + row_offset: &mut usize, + iv_constants: &[AssignedBlake2bWord; 8], + global_state: &mut [AssignedBlake2bWord; 8], + current_block: [AssignedBlake2bWord; 16], + processed_bytes_count: u64, + is_last_block: bool, + ) -> Result<[AssignedByte; 64], Error>; + + /// This method computes a single round of mixing for the Blake2b algorithm. + /// One round of compress has 96 mixing rounds. + /// 'x' and 'y' are the variables that hold the AssignedCell with the input values that will + /// be processed in this mixing round. + /// The 'state_indexes' are the indexes of the compress state that will take part on this + /// mixing round. These are also needed to update the state at the end of the mixing. + fn mix( + &self, + state_indexes: [usize; 4], + x: AssignedBlake2bWord, + y: AssignedBlake2bWord, + state: &mut [AssignedBlake2bWord; 16], + region: &mut Region<'_, F>, + offset: &mut usize, + ) -> Result<(), Error>; +} diff --git a/third_party/blake2b_halo2/src/blake2b/mod.rs b/third_party/blake2b_halo2/src/blake2b/mod.rs new file mode 100644 index 000000000..86a2e9bf0 --- /dev/null +++ b/third_party/blake2b_halo2/src/blake2b/mod.rs @@ -0,0 +1,22 @@ +//! A chip defining a Blake2b hash invocation. This interface works with in/out consisting of +//! AssignedNative. The algorithm expects its values to be in the range of a Byte, and will fail if +//! they're not. +//! +//! The chip relies on a set of basic instructions, implemented as a trait called +//! [Blake2bInstructions]. There is currently one implementation of the instruction set: +//! * [Blake2bChip] This chip uses a lookup table of size `2**16`. This means +//! that all circuits instantiating this chip will be at least `2**17` rows, +//! as we need to padd the circuit to provide ZK. This chip achieves a Blake2b +//! digest in 2469 rows. + +/// This is the trait that contains most of the behaviour of the blake2b chips. +pub(crate) mod blake2b_instructions; + +/// Basic definitions and constants for the blake2b chip. +pub(crate) mod utils; + +/// These are the separated optimizations. +pub mod blake2b_chip; + +/// Number of advice columns required by the chip. +pub const NB_BLAKE2B_ADVICE_COLS: usize = 9; diff --git a/third_party/blake2b_halo2/src/blake2b/utils.rs b/third_party/blake2b_halo2/src/blake2b/utils.rs new file mode 100644 index 000000000..e9f5b527a --- /dev/null +++ b/third_party/blake2b_halo2/src/blake2b/utils.rs @@ -0,0 +1,166 @@ +use crate::types::AssignedNative; +use ff::PrimeField; +use midnight_proofs::circuit::Region; +use midnight_proofs::plonk::Error; +use crate::types::blake2b_word::AssignedBlake2bWord; +use crate::types::row::AssignedRow; + +/// Enforces the output and key sizes. +/// Output size must be between 1 and 64 bytes. +/// Key size must be between 0 and 64 bytes. +pub(crate) fn enforce_input_sizes(output_size: usize, key_size: usize) { + assert!(output_size <= 64, "Output size must be between 1 and 64 bytes"); + assert!(output_size > 0, "Output size must be between 1 and 64 bytes"); + assert!(key_size <= 64, "Key size must be between 1 and 64 bytes"); +} + +/// Extracts the full number cell of each of the state rows +pub(crate) fn full_number_of_each_state_row( + current_block_rows: [AssignedRow; 16], +) -> [AssignedBlake2bWord; 16] { + current_block_rows + .iter() + .map(|row| row.full_number.clone()) + .collect::>() + .try_into() + .unwrap() +} + +/// The 'processed_bytes_count' is a variable in the algorithm that changes with every iteration, +/// in each iteration we compute the new value for it. +pub(crate) fn compute_processed_bytes_count_value_for_iteration( + iteration: usize, + is_last_block: bool, + input_size: usize, + empty_key: bool, +) -> u64 { + let processed_bytes_count = if is_last_block { + input_size + if empty_key { 0 } else { 128 } + } else { + 128 * (iteration + 1) + }; + + processed_bytes_count as u64 +} + +/// This function will return 0 in most cases, except in 3 opportunities: +/// 1 - In the "key block": the first block (if there's a key) +/// 2 - In the last block, if the input length in bytes isn't a multiple of 128 +/// 3 - If the input and key are empty, the algorithm will compute a single block of 128 zeros +pub(crate) fn zeros_to_pad_in_current_block( + key: &[AssignedNative], + input_size: usize, + is_last_block: bool, + is_key_block: bool, +) -> usize { + if is_last_block && !is_key_block { + if input_size == 0 { + // Border case, the input and the key are empty + 128 + } else { + // Last block, need to complete the block with zeroes + (BLAKE2B_BLOCK_SIZE - input_size % BLAKE2B_BLOCK_SIZE) % BLAKE2B_BLOCK_SIZE + } + } else if is_key_block { + // First block when there's a key, need to complete the block with zeroes + BLAKE2B_BLOCK_SIZE - key.len() + } else { + // Middle block, no need to pad anything + 0 + } +} + +/// Computes the edge cases in the amount of blocks to process. +pub(crate) fn get_total_blocks_count( + input_blocks: usize, + is_input_empty: bool, + is_key_empty: bool, +) -> usize { + if is_key_empty { + if is_input_empty { + // If there's no input and no key, we still need to process one block of zeroes. + 1 + } else { + input_blocks + } + } else if is_input_empty { + // If there's no input but there's key, key is processed in the first and only block. + 1 + } else { + // Key needs to be processed in a block alone, then come the input blocks. + input_blocks + 1 + } +} + +/// This method constrains the padding cells to equal zero. The amount of constraints +/// depends on the input size and the key size, which makes sense since those values are known +/// at circuit building time. +/// The idea is that since we decompose the state into 8 limbs, we already have the input +/// bytes in the trace. It's just a matter of iterating the cells in the correct order and knowing +/// which ones should equal zero. In Blake2b the padding is allways 0. +pub(crate) fn constrain_padding_cells_to_equal_zero( + region: &mut Region<'_, F>, + zeros_amount: usize, + current_block_rows: &[AssignedRow; 16], + zero_constant_cell: &AssignedNative, +) -> Result<(), Error> { + let mut constrained_padding_cells = 0; + for row in (0..16).rev() { + for limb in (0..8).rev() { + if constrained_padding_cells < zeros_amount { + region.constrain_equal( + current_block_rows[row].limbs[limb].cell(), + zero_constant_cell.cell(), + )?; + constrained_padding_cells += 1; + } + } + } + Ok(()) +} + +// ----- Blake2b constants ----- + +pub const BLAKE2B_BLOCK_SIZE: usize = 128; + +pub const SIGMA: [[usize; 16]; 12] = [ + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], + [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4], + [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8], + [9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13], + [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9], + [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11], + [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10], + [6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5], + [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], +]; + +/// These are constants used for the mixing rounds: +/// In each round, Blake2b algorithm modifies 4 fixed components of the state. +/// The first round, these components are +/// state[0], state[4], state[8], state[12], the second round, they're +/// state[1], state[5], state[9], state[13], and so on. +pub const ABCD: [[usize; 4]; 8] = [ + [0, 4, 8, 12], + [1, 5, 9, 13], + [2, 6, 10, 14], + [3, 7, 11, 15], + [0, 5, 10, 15], + [1, 6, 11, 12], + [2, 7, 8, 13], + [3, 4, 9, 14], +]; + +pub const IV_CONSTANTS: [u64; 8] = [ + 0x6A09E667F3BCC908u64, + 0xBB67AE8584CAA73Bu64, + 0x3C6EF372FE94F82Bu64, + 0xA54FF53A5F1D36F1u64, + 0x510E527FADE682D1u64, + 0x9B05688C2B3E6C1Fu64, + 0x1F83D9ABFB41BD6Bu64, + 0x5BE0CD19137E2179u64, +]; diff --git a/third_party/blake2b_halo2/src/lib.rs b/third_party/blake2b_halo2/src/lib.rs new file mode 100644 index 000000000..e61b66445 --- /dev/null +++ b/third_party/blake2b_halo2/src/lib.rs @@ -0,0 +1,19 @@ +//! Halo2 Blake2b implementation. +#![deny(missing_docs)] +#![deny(missing_debug_implementations)] +#![deny(rust_2018_idioms)] + +use midnight_proofs::{circuit::Layouter, plonk::ConstraintSystem}; + +use ff::PrimeField; +use midnight_proofs::circuit::{Region, Value}; +use midnight_proofs::plonk::{Advice, Column, Error, Expression, Selector, TableColumn}; +use midnight_proofs::poly::Rotation; + +pub(crate) mod base_operations; + +#[cfg(test)] +mod tests; +pub mod blake2b; +pub mod types; +pub mod usage_utils; diff --git a/third_party/blake2b_halo2/src/tests/mod.rs b/third_party/blake2b_halo2/src/tests/mod.rs new file mode 100644 index 000000000..1b93aa248 --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/mod.rs @@ -0,0 +1,151 @@ +use super::*; +use midnight_curves::bls12_381::Fq; +use ff::Field; +use std::marker::PhantomData; +use crate::base_operations::{ + create_limb_decomposition_gate, create_range_check_gate, generate_row_from_word_and_keep_row, + populate_lookup_table, +}; +use crate::types::AssignedNative; +use crate::types::blake2b_word::Blake2bWord; +use crate::types::row::AssignedRow; + +mod test_blake2b; +mod test_negate; +mod tests_addition; +mod tests_rotation; +mod tests_xor; + +pub(crate) fn one() -> Value { + Value::known(Fq::ONE) +} +pub(crate) fn zero() -> Value { + Value::known(Fq::ZERO) +} + +pub(crate) fn blake2b_value_for(number: u64) -> Value { + Value::known(number.into()) +} + +pub(crate) fn value_for(number: T) -> Value +where + T: Into, + F: PrimeField, +{ + Value::known(F::from_u128(number.into())) +} + +pub(crate) fn generate_row_8bits(number: T) -> [Value; 9] +where + F: PrimeField, + T: Into, +{ + let mut number: u128 = number.into(); + let mut ans = [Value::unknown(); 9]; + ans[0] = value_for(number); + for ans_item in ans.iter_mut().take(9).skip(1) { + *ans_item = value_for(number % 256); + number /= 256; + } + ans +} + +/// This config handles the decomposition of 64-bit numbers into 8-bit limbs in the trace, +/// where each limbs is range checked regarding the designated limb size. +/// T is the amount of limbs that the number will be decomposed into. +/// Little endian representation is used for the limbs. +/// We also expect F::Repr to be little endian in all usages of this trait. +#[derive(Clone, Debug)] +struct Decompose8Config { + /// The full number and the limbs are not owned by the config. + full_number_u64: Column, + /// There are 8 limbs of 8 bits each + limbs: [Column; 8], + + /// Selector that turns on the gate that defines if the limbs should add up to the full number + q_decompose: Selector, + + /// Selector that turns on the gate that defines if the limbs should be range-checked + q_range: Selector, + + /// Table of [0, 2^8) to check if the limb is in the correct range + t_range: TableColumn, +} + +impl Decompose8Config { + /// Creates the corresponding gates and lookups to constrain range-checks and 8-limb + /// decomposition of 64-bit numbers. + fn configure( + meta: &mut ConstraintSystem, + // The full number and the limbs are not owned by the config. + full_number_u64: Column, + limbs: [Column; 8], + ) -> Self { + let q_range = meta.complex_selector(); + let t_range = meta.lookup_table_column(); + let q_decompose = meta.complex_selector(); + + // Gate that checks if the decomposition is correct + create_limb_decomposition_gate(meta, q_decompose, full_number_u64, limbs); + + // Range checks for all the limbs (range [0,255]) + create_range_check_gate(meta, t_range, q_range, limbs); + + Self { + full_number_u64, + limbs, + q_decompose, + t_range, + q_range, + } + } + + /// Fills the [t_range] table with values in the range [0,255] + fn populate_lookup_table( + &self, + layouter: &mut impl Layouter, + ) -> Result<(), Error> { + populate_lookup_table(layouter, self.t_range) + } + /// Given an explicit vector of values, it assigns the full number and the limbs in a row of the trace + /// row size is T + 1 + /// row[0] is the full number + /// row[1..T] are the limbs representation of row[0] + fn populate_row_from_values( + &self, + region: &mut Region<'_, F>, + row: &[Value], + offset: usize, + check_decomposition: bool, + ) -> Result>, Error> { + if check_decomposition { + self.q_decompose.enable(region, offset)?; + self.q_range.enable(region, offset)?; + } + let full_number = + region.assign_advice(|| "full number", self.full_number_u64, offset, || row[0])?; + + let limbs = (0..8) + .map(|i| { + region.assign_advice(|| format!("limb{}", i), self.limbs[i], offset, || row[i + 1]) + }) + .collect::, _>>()?; + + //return the full number and the limbs + Ok(std::iter::once(full_number).chain(limbs).collect()) + } + + /// Method for generating a row from a value and keeping the full row. + /// Given a Value, we might want to use it as an operand in the circuit, and sometimes we need + /// to establish constraints over the result's limbs. That's why we need a way to retrieve the + /// full row that was created from that value. + fn generate_row_from_word_and_keep_row( + &self, + region: &mut Region<'_, F>, + value: Value, + offset: usize, + ) -> Result, Error> { + self.q_range.enable(region, offset)?; + generate_row_from_word_and_keep_row(region, value, offset, self.full_number_u64, self.limbs) + } +} diff --git a/third_party/blake2b_halo2/src/tests/test_blake2b/circuit_in_production.rs b/third_party/blake2b_halo2/src/tests/test_blake2b/circuit_in_production.rs new file mode 100644 index 000000000..b6f5210d4 --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/test_blake2b/circuit_in_production.rs @@ -0,0 +1,25 @@ +use super::*; +use crate::usage_utils::circuit_runner::CircuitRunner; + +#[test] +fn test_with_real_snark() { + let input = String::from("0001"); + let out = String::from("1c08798dc641aba9dee435e22519a4729a09b2bfe0ff00ef2dcd8ed6f8a07d15eaf4aee52bbf18ab5608a6190f70b90486c8a7d4873710b1115d3debbb4327b5"); + let key = String::from(""); + + assert!(test_in_production(input, out, key).is_ok()); +} + +#[test] +#[should_panic] +fn test_negative_with_real_snark() { + let input = String::from("0001"); + let out = String::from("2c08798dc641aba9dee435e22519a4729a09b2bfe0ff00ef2dcd8ed6f8a07d15eaf4aee52bbf18ab5608a6190f70b90486c8a7d4873710b1115d3debbb4327b5"); + let key = String::from(""); + + assert!(test_in_production(input, out, key).is_ok()); +} + +fn test_in_production(input: String, out: String, key: String) -> Result<(), Error> { + CircuitRunner::real_preprocess_inputs_synthesize_prove_and_verify(input, out, key) +} diff --git a/third_party/blake2b_halo2/src/tests/test_blake2b/mod.rs b/third_party/blake2b_halo2/src/tests/test_blake2b/mod.rs new file mode 100644 index 000000000..8f2a968ed --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/test_blake2b/mod.rs @@ -0,0 +1,7 @@ +use super::*; + +mod smoke_tests; +mod vector_tests; +mod variable_output_length_tests; +mod variable_key_length_tests; +mod circuit_in_production; diff --git a/third_party/blake2b_halo2/src/tests/test_blake2b/smoke_tests.rs b/third_party/blake2b_halo2/src/tests/test_blake2b/smoke_tests.rs new file mode 100644 index 000000000..8506274ba --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/test_blake2b/smoke_tests.rs @@ -0,0 +1,99 @@ +use crate::usage_utils::circuit_runner::CircuitRunner; +use super::*; + +#[test] +fn test_blake2b_single_empty_block_positive() { + let output_size = 64; + let input = vec![]; + let input_size = 0; + let expected_output_state = correct_output_for_empty_input_64(); + + let circuit = + CircuitRunner::create_circuit_for_inputs(input, input_size, vec![], 0, output_size); + let prover = CircuitRunner::mock_prove_with_public_inputs_ref(&expected_output_state, &circuit); + CircuitRunner::verify_mock_prover(prover); +} + +#[test] +#[should_panic] +fn test_blake2b_single_empty_block_negative() { + let output_size = 64; + let input = vec![]; + let input_size = 0; + let mut expected_output_state = correct_output_for_empty_input_64(); + expected_output_state[7] = Fq::from(14u64); // Wrong value + + let circuit = + CircuitRunner::create_circuit_for_inputs(input, input_size, vec![], 0, output_size); + let prover = CircuitRunner::mock_prove_with_public_inputs_ref(&expected_output_state, &circuit); + CircuitRunner::verify_mock_prover(prover); +} + +fn correct_output_for_empty_input_64() -> [Fq; 64] { + [ + Fq::from(120), + Fq::from(106), + Fq::from(2), + Fq::from(247), + Fq::from(66), + Fq::from(1), + Fq::from(89), + Fq::from(3), + Fq::from(198), + Fq::from(198), + Fq::from(253), + Fq::from(133), + Fq::from(37), + Fq::from(82), + Fq::from(210), + Fq::from(114), + Fq::from(145), + Fq::from(47), + Fq::from(71), + Fq::from(64), + Fq::from(225), + Fq::from(88), + Fq::from(71), + Fq::from(97), + Fq::from(138), + Fq::from(134), + Fq::from(226), + Fq::from(23), + Fq::from(247), + Fq::from(31), + Fq::from(84), + Fq::from(25), + Fq::from(210), + Fq::from(94), + Fq::from(16), + Fq::from(49), + Fq::from(175), + Fq::from(238), + Fq::from(88), + Fq::from(83), + Fq::from(19), + Fq::from(137), + Fq::from(100), + Fq::from(68), + Fq::from(147), + Fq::from(78), + Fq::from(176), + Fq::from(75), + Fq::from(144), + Fq::from(58), + Fq::from(104), + Fq::from(91), + Fq::from(20), + Fq::from(72), + Fq::from(183), + Fq::from(85), + Fq::from(213), + Fq::from(111), + Fq::from(112), + Fq::from(26), + Fq::from(254), + Fq::from(155), + Fq::from(226), + Fq::from(206), + ] +} diff --git a/third_party/blake2b_halo2/src/tests/test_blake2b/variable_key_length_tests.rs b/third_party/blake2b_halo2/src/tests/test_blake2b/variable_key_length_tests.rs new file mode 100644 index 000000000..8357f39d5 --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/test_blake2b/variable_key_length_tests.rs @@ -0,0 +1,16 @@ +use crate::usage_utils::circuit_runner::CircuitRunner; +use super::*; + +#[test] +#[should_panic(expected = "Key size must be between 1 and 64 bytes")] +fn test_blake2b_circuit_should_receive_an_key_length_less_or_equal_64() { + let input = vec![]; + let input_size = 0; + let key: Vec> = vec![value_for(0u64); 65]; + let key_size = 65; + + let expected_output_state = [Fq::ZERO; 65]; + let circuit = CircuitRunner::create_circuit_for_inputs(input, input_size, key, key_size, 64); + let prover = CircuitRunner::mock_prove_with_public_inputs_ref(&expected_output_state, &circuit); + CircuitRunner::verify_mock_prover(prover); +} diff --git a/third_party/blake2b_halo2/src/tests/test_blake2b/variable_output_length_tests.rs b/third_party/blake2b_halo2/src/tests/test_blake2b/variable_output_length_tests.rs new file mode 100644 index 000000000..47c49250f --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/test_blake2b/variable_output_length_tests.rs @@ -0,0 +1,90 @@ +use crate::usage_utils::circuit_runner::CircuitRunner; +use super::*; + +#[test] +fn test_blake2b_circuit_can_verify_an_output_of_length_1() { + let output_size = 1; + let input = vec![]; + let input_size = 0; + let expected_output_state = correct_output_for_empty_input_1(); + run_variable_output_length_test(output_size, input, input_size, expected_output_state); +} + +#[test] +#[should_panic] +fn test_blake2b_circuit_can_verify_an_output_of_length_1_negative() { + let output_size = 1; + let input = vec![]; + let input_size = 0; + let mut expected_output_state = correct_output_for_empty_input_1(); + expected_output_state[0] = Fq::from(14u64); // Wrong value + run_variable_output_length_test(output_size, input, input_size, expected_output_state); +} + +#[test] +fn test_blake2b_circuit_can_verify_an_output_of_length_32() { + let output_size = 32; + let input = vec![]; + let input_size = 0; + let expected_output_state = correct_output_for_empty_input_32(); + run_variable_output_length_test(output_size, input, input_size, expected_output_state); +} + +#[test] +#[should_panic] +fn test_blake2b_circuit_can_verify_an_output_of_length_32_negative() { + let output_size = 32; + let input = vec![]; + let input_size = 0; + let mut expected_output_state = correct_output_for_empty_input_32(); + expected_output_state[0] = Fq::from(15u64); // Wrong value + run_variable_output_length_test(output_size, input, input_size, expected_output_state); +} + +#[test] +#[should_panic(expected = "Output size must be between 1 and 64 bytes")] +fn test_blake2b_circuit_should_receive_an_output_length_less_or_equal_64() { + let output_size = 65; + let input = vec![]; + let input_size = 0; + let expected_output_state = [Fq::ZERO; 65]; + run_variable_output_length_test(output_size, input, input_size, expected_output_state); +} + +#[test] +#[should_panic(expected = "Output size must be between 1 and 64 bytes")] +fn test_blake2b_circuit_should_receive_an_output_length_bigger_or_equal_1() { + let output_size = 0; + let input = vec![]; + let input_size = 0; + let expected_output_state = [Fq::ZERO; 65]; + run_variable_output_length_test(output_size, input, input_size, expected_output_state); +} + +fn run_variable_output_length_test( + output_size: usize, + input: Vec>, + input_size: usize, + expected_output_state: [Fq; OUT_SIZE], +) { + let circuit = + CircuitRunner::create_circuit_for_inputs(input, input_size, vec![], 0, output_size); + let prover = CircuitRunner::mock_prove_with_public_inputs_ref(&expected_output_state, &circuit); + CircuitRunner::verify_mock_prover(prover); +} + +fn correct_output_for_empty_input_1() -> [Fq; 1] { + [Fq::from(46)] +} + +fn correct_output_for_empty_input_32() -> [Fq; 32] { + [ + 14, 87, 81, 192, 38, 229, 67, 178, 232, 171, 46, 176, 96, 153, 218, 161, 209, 229, 223, 71, + 119, 143, 119, 135, 250, 171, 69, 205, 241, 47, 227, 168, + ] + .iter() + .map(|x| Fq::from(*x as u64)) + .collect::>() + .try_into() + .unwrap() +} diff --git a/third_party/blake2b_halo2/src/tests/test_blake2b/vector_tests.rs b/third_party/blake2b_halo2/src/tests/test_blake2b/vector_tests.rs new file mode 100644 index 000000000..f4c278ec6 --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/test_blake2b/vector_tests.rs @@ -0,0 +1,66 @@ +use serde::Deserialize; +use crate::usage_utils::circuit_runner::CircuitRunner; + +#[derive(Deserialize, Debug)] +struct TestCase { + #[serde(rename = "in")] + input: String, + key: String, + out: String, +} + +pub(crate) fn run_test(input: &String, key: &String, expected: &String) { + CircuitRunner::mocked_preprocess_inputs_synthesize_prove_and_verify(input, key, expected); +} + +#[test] +fn test_hashes_in_circuit_one_block() { + let test_cases = obtain_test_cases(); + + for (i, case) in test_cases.iter().enumerate() { + if !case.key.is_empty() || case.input.len() > 256 { + continue; + } + + println!("Running test case {}", i); + run_test(&case.input, &case.key, &case.out); + } +} + +#[test] +fn test_hashes_in_circuit_more_than_one_block() { + let test_cases = obtain_test_cases(); + + for (i, case) in test_cases.iter().enumerate() { + if !case.key.is_empty() || case.input.len() <= 256 { + continue; + } + + println!("Running test case {}", i); + run_test(&case.input, &case.key, &case.out); + } +} + +#[test] +fn test_hashes_in_circuit_with_key() { + let test_cases = obtain_test_cases(); + + for (i, case) in test_cases.iter().enumerate() { + if case.key.is_empty() { + continue; + } + + // Uncomment to run representative tests of edge cases + // if i != 256 && i != 257 && i != 384 && i != 385 { + // continue; + // } + + println!("Running test case {}", i); + run_test(&case.input, &case.key, &case.out); + } +} + +fn obtain_test_cases() -> Vec { + let file_content = std::fs::read_to_string("./test_vector.json").expect("Failed to read file"); + serde_json::from_str(&file_content).expect("Failed to parse JSON") +} diff --git a/third_party/blake2b_halo2/src/tests/test_negate/mod.rs b/third_party/blake2b_halo2/src/tests/test_negate/mod.rs new file mode 100644 index 000000000..9b23886eb --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/test_negate/mod.rs @@ -0,0 +1,38 @@ +use super::*; +mod negate_circuit; + +use crate::tests::test_negate::negate_circuit::NegateCircuit; +use midnight_proofs::dev::MockProver; + +#[test] +fn test_negate_zero_should_result_in_max_number() { + let x = blake2b_value_for(0); + let not_x = blake2b_value_for(((1u128 << 64) - 1) as u64); + + let circuit = NegateCircuit::::new_for(x, not_x); + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +#[test] +fn test_negate_number_should_result_in_max_number_minus_that_number() { + let number = ((1u128 << 40) - 1) as u64; + let max_number = ((1u128 << 64) - 1) as u64; + let not_x = blake2b_value_for(max_number - number); + + let circuit = NegateCircuit::::new_for(blake2b_value_for(number), not_x); + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +#[test] +#[should_panic] +fn test_negate_number_fails_when_given_a_wrong_result() { + let number = (1u64 << 40) - 1; + let max_number = ((1u128 << 64) - 1) as u64; + let not_x = blake2b_value_for(max_number - number - 1); + + let circuit = NegateCircuit::::new_for(blake2b_value_for(number), not_x); + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} diff --git a/third_party/blake2b_halo2/src/tests/test_negate/negate_circuit.rs b/third_party/blake2b_halo2/src/tests/test_negate/negate_circuit.rs new file mode 100644 index 000000000..d3d9c8ac5 --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/test_negate/negate_circuit.rs @@ -0,0 +1,106 @@ +use crate::tests::Decompose8Config; +use crate::base_operations::negate::NegateConfig; +use crate::types::blake2b_word::Blake2bWord; +use ff::PrimeField; +use midnight_proofs::circuit::{Layouter, SimpleFloorPlanner, Value}; +use midnight_proofs::plonk::{Advice, Circuit, Column, ConstraintSystem, Error, Fixed}; +use std::array; +use std::marker::PhantomData; + +pub(crate) struct NegateCircuit { + _ph: PhantomData, + value: Value, + expected_result: Value, +} + +#[derive(Clone)] +pub(crate) struct NegateCircuitConfig { + _ph: PhantomData, + negate_config: NegateConfig, + decompose_8_config: Decompose8Config, + fixed_result: Column, + full_number_u64: Column, +} + +impl Circuit for NegateCircuit { + type Config = NegateCircuitConfig; + type Params = (); + type FloorPlanner = SimpleFloorPlanner; + + fn without_witnesses(&self) -> Self { + Self { + _ph: PhantomData, + value: Value::unknown(), + expected_result: Value::unknown(), + } + } + + #[allow(unused_variables)] + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + let full_number_u64 = meta.advice_column(); + let limbs: [Column; 8] = array::from_fn(|_| meta.advice_column()); + + let decompose_8_config = Decompose8Config::configure(meta, full_number_u64, limbs); + let negate_config = NegateConfig::configure(meta, full_number_u64); + + let fixed_result = meta.fixed_column(); + meta.enable_equality(full_number_u64); + meta.enable_equality(fixed_result); + + Self::Config { + _ph: PhantomData, + negate_config, + decompose_8_config, + fixed_result, + full_number_u64, + } + } + + #[allow(unused_variables)] + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + config.decompose_8_config.populate_lookup_table(&mut layouter)?; + + layouter.assign_region( + || "negate", + |mut region| { + let mut offset = 0; + let cell = config + .decompose_8_config + .generate_row_from_word_and_keep_row(&mut region, self.value, offset)? + .full_number; + offset += 1; + + let result = config.negate_config.generate_rows_from_cell( + &mut region, + &mut offset, + &cell, + config.full_number_u64, + )?; + let fixed_cell = region.assign_fixed( + || "assign fixed", + config.fixed_result, + 0, + || self.expected_result, + )?; + region.constrain_equal(result.cell(), fixed_cell.cell())?; + Ok(()) + }, + )?; + + Ok(()) + } +} + +impl NegateCircuit { + pub(crate) fn new_for(value: Value, expected_result: Value) -> Self { + Self { + _ph: PhantomData, + value, + expected_result, + } + } +} diff --git a/third_party/blake2b_halo2/src/tests/tests_addition/addition_mod_64_circuit_8bits.rs b/third_party/blake2b_halo2/src/tests/tests_addition/addition_mod_64_circuit_8bits.rs new file mode 100644 index 000000000..7127a7ec1 --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/tests_addition/addition_mod_64_circuit_8bits.rs @@ -0,0 +1,76 @@ +use super::*; +use crate::tests::Decompose8Config; +use midnight_proofs::circuit::SimpleFloorPlanner; +use midnight_proofs::plonk::Circuit; +use std::array; + +pub(crate) struct AdditionMod64Circuit8Bits { + _ph: PhantomData, + trace: [[Value; 9]; 3], +} + +#[derive(Clone, Debug)] +pub(crate) struct AdditionMod64Config8Bits { + sum_8bits_config: AdditionMod64Config, + decompose_8_config: Decompose8Config, + _ph: PhantomData, +} + +impl Circuit for AdditionMod64Circuit8Bits { + type Config = AdditionMod64Config8Bits; + type Params = (); + type FloorPlanner = SimpleFloorPlanner; + + fn without_witnesses(&self) -> Self { + Self { + _ph: PhantomData, + trace: [[Value::unknown(); 9]; 3], + } + } + + #[allow(unused_variables)] + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + let full_number_u64 = meta.advice_column(); + let limbs: [Column; 8] = array::from_fn(|_| meta.advice_column()); + + let decompose_8_config = Decompose8Config::configure(meta, full_number_u64, limbs); + + let sum_8bits_config = AdditionMod64Config::configure( + meta, + full_number_u64, + limbs[0], + decompose_8_config.q_decompose, + decompose_8_config.q_range, + ); + + Self::Config { + _ph: PhantomData, + decompose_8_config, + sum_8bits_config, + } + } + + #[allow(unused_variables)] + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + config.decompose_8_config.populate_lookup_table(&mut layouter)?; + config.sum_8bits_config.populate_addition_rows( + &mut layouter, + self.trace, + config.decompose_8_config.clone(), + )?; + Ok(()) + } +} + +impl AdditionMod64Circuit8Bits { + pub(crate) fn new_for_trace(trace: [[Value; 9]; 3]) -> Self { + Self { + _ph: PhantomData, + trace, + } + } +} diff --git a/third_party/blake2b_halo2/src/tests/tests_addition/addition_mod_64_circuit_8bits_autogenerated.rs b/third_party/blake2b_halo2/src/tests/tests_addition/addition_mod_64_circuit_8bits_autogenerated.rs new file mode 100644 index 000000000..5aca2dfcc --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/tests_addition/addition_mod_64_circuit_8bits_autogenerated.rs @@ -0,0 +1,174 @@ +use super::*; + +use midnight_proofs::circuit::SimpleFloorPlanner; +use midnight_proofs::plonk::{Circuit, Fixed}; +use std::array; +use crate::types::bit::AssignedBit; +use crate::types::blake2b_word::{AssignedBlake2bWord, Blake2bWord}; +use crate::tests::Decompose8Config; + +pub(crate) struct AdditionMod64Circuit8BitsAutogenerated { + _ph: PhantomData, + value_a: Value, + value_b: Value, + expected_result: Value, + expected_carry: Value, +} + +#[derive(Clone)] +pub(crate) struct AdditionMod64Config8BitsWithResultValidation { + _ph: PhantomData, + add_config: AdditionMod64Config8Bits, + fixed_result: Column, + fixed_carry: Column, + full_number_u64: Column, + limbs: [Column; 8], +} + +impl Circuit for AdditionMod64Circuit8BitsAutogenerated { + type Config = AdditionMod64Config8BitsWithResultValidation; + type Params = (); + type FloorPlanner = SimpleFloorPlanner; + + fn without_witnesses(&self) -> Self { + Self { + _ph: PhantomData, + value_a: Value::unknown(), + value_b: Value::unknown(), + expected_result: Value::unknown(), + expected_carry: Value::unknown(), + } + } + + #[allow(unused_variables)] + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + let full_number_u64 = meta.advice_column(); + let limbs: [Column; 8] = array::from_fn(|_| meta.advice_column()); + + let decompose_8_config = Decompose8Config::configure(meta, full_number_u64, limbs); + let addition_config = AdditionMod64Config::configure( + meta, + full_number_u64, + limbs[0], + decompose_8_config.q_decompose, + decompose_8_config.q_range, + ); + + let fixed_result = meta.fixed_column(); + let fixed_carry = meta.fixed_column(); + meta.enable_equality(full_number_u64); + meta.enable_equality(limbs[0]); + meta.enable_equality(fixed_result); + meta.enable_equality(fixed_carry); + + Self::Config { + _ph: PhantomData, + add_config: AdditionMod64Config8Bits { + _ph: PhantomData, + sum_8bits_config: addition_config, + decompose_8_config, + }, + fixed_result, + fixed_carry, + full_number_u64, + limbs, + } + } + + #[allow(unused_variables)] + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + config.add_config.decompose_8_config.populate_lookup_table(&mut layouter)?; + layouter.assign_region( + || "sum", + |mut region| { + let mut offset = 0; + let a_cell = config + .add_config + .decompose_8_config + .generate_row_from_word_and_keep_row(&mut region, self.value_a, offset)? + .full_number; + offset += 1; + let b_cell = config + .add_config + .decompose_8_config + .generate_row_from_word_and_keep_row(&mut region, self.value_b, offset)? + .full_number; + offset += 1; + + let decompose = &config.add_config.decompose_8_config; + let result_and_carry = + config.add_config.sum_8bits_config.generate_addition_rows_from_cells( + &mut region, + &mut offset, + &a_cell, + &b_cell, + false, + config.full_number_u64, + config.limbs, + )?; + + Self::assert_word_value( + &mut region, + &result_and_carry.0.full_number, + config.fixed_result, + self.expected_result, + )?; + Self::assert_carry_value( + &mut region, + &result_and_carry.1, + config.fixed_carry, + self.expected_carry, + )?; + Ok(()) + }, + )?; + Ok(()) + } +} + +impl AdditionMod64Circuit8BitsAutogenerated { + fn assert_word_value( + region: &mut Region<'_, F>, + cell: &AssignedBlake2bWord, + fixed_column: Column, + expected_value: Value, + ) -> Result<(), Error> { + let fixed_cell = + region.assign_fixed(|| "assign fixed", fixed_column, 0, || expected_value)?; + region.constrain_equal(cell.cell(), fixed_cell.cell())?; + Ok(()) + } + + fn assert_carry_value( + region: &mut Region<'_, F>, + cell: &AssignedBit, + fixed_column: Column, + expected_value: Value, + ) -> Result<(), Error> { + let fixed_cell = + region.assign_fixed(|| "assign fixed", fixed_column, 0, || expected_value)?; + region.constrain_equal(cell.cell(), fixed_cell.cell())?; + Ok(()) + } +} + +impl AdditionMod64Circuit8BitsAutogenerated { + pub(crate) fn new_for( + value_a: Value, + value_b: Value, + expected_result: Value, + expected_carry: Value, + ) -> Self { + Self { + _ph: PhantomData, + value_a, + value_b, + expected_result, + expected_carry, + } + } +} diff --git a/third_party/blake2b_halo2/src/tests/tests_addition/mod.rs b/third_party/blake2b_halo2/src/tests/tests_addition/mod.rs new file mode 100644 index 000000000..447cc2a19 --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/tests_addition/mod.rs @@ -0,0 +1,50 @@ +use super::*; +use crate::base_operations::addition_mod_64::AdditionMod64Config; +use crate::tests::Decompose8Config; + +mod test_addition_mod_64_with_autogenerated_trace; +mod test_addition_mod_64_with_processed_trace; + +mod addition_mod_64_circuit_8bits; +mod addition_mod_64_circuit_8bits_autogenerated; + +#[derive(Clone, Debug)] +pub(crate) struct AdditionMod64Config8Bits { + sum_8bits_config: AdditionMod64Config, + decompose_8_config: Decompose8Config, + _ph: PhantomData, +} + +impl AdditionMod64Config { + /// This method is meant to receive a valid addition_trace, and populate the circuit with it + /// The addition trace is a matrix with 3 rows and R columns. The rows represent the two + /// parameters of the addition and its result. + /// Each row has the following format: + /// [full_number, limb_0, ..., limb_R-2, carry] + /// Note that the carry value is not used in the parameters of the addition, but it is used + /// to calculate its result. + fn populate_addition_rows( + &self, + layouter: &mut impl Layouter, + addition_trace: [[Value; 9]; 3], + decompose8config: Decompose8Config, + ) -> Result<(), Error> { + layouter.assign_region( + || "sum", + |mut region| { + self.q_add.enable(&mut region, 0)?; + + let row = addition_trace[0].to_vec(); + decompose8config.populate_row_from_values(&mut region, &row, 0, false)?; + + let row = addition_trace[1].to_vec(); + decompose8config.populate_row_from_values(&mut region, &row, 1, false)?; + + let row = addition_trace[2].to_vec(); + decompose8config.populate_row_from_values(&mut region, &row, 2, true)?; + Ok(()) + }, + )?; + Ok(()) + } +} diff --git a/third_party/blake2b_halo2/src/tests/tests_addition/test_addition_mod_64_with_autogenerated_trace.rs b/third_party/blake2b_halo2/src/tests/tests_addition/test_addition_mod_64_with_autogenerated_trace.rs new file mode 100644 index 000000000..a3165f103 --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/tests_addition/test_addition_mod_64_with_autogenerated_trace.rs @@ -0,0 +1,49 @@ +use crate::tests::tests_addition::addition_mod_64_circuit_8bits_autogenerated::AdditionMod64Circuit8BitsAutogenerated; +use midnight_proofs::dev::MockProver; +use midnight_curves::bls12_381::Fq; +use crate::tests::blake2b_value_for; + +#[test] +fn test_positive_addition() { + let a = 150u64; + let b = 50u64; + let expected_result = 200u64; + let expected_carry = 0u64; + run_test_for_addition(a, b, expected_result, expected_carry); +} +#[test] +#[should_panic] +fn test_wrong_addition() { + let a = 150u64; + let b = 30u64; + let expected_result = 200u64; + let expected_carry = 0u64; + run_test_for_addition(a, b, expected_result, expected_carry); +} + +#[test] +fn test_positive_addition_with_0() { + let a = 150u64; + let b = 0u64; + run_test_for_addition(a, b, a, 0u64); +} + +#[test] +fn test_positive_addition_with_carry() { + let a = 2593u64; + let b = ((1u128 << 64) - 1) as u64; + run_test_for_addition(a, b, a - 1, 1u64); +} + +// aux + +fn run_test_for_addition(a: u64, b: u64, expected_result: u64, expected_carry: u64) { + let circuit = AdditionMod64Circuit8BitsAutogenerated::::new_for( + blake2b_value_for(a), + blake2b_value_for(b), + blake2b_value_for(expected_result), + blake2b_value_for(expected_carry), + ); + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} diff --git a/third_party/blake2b_halo2/src/tests/tests_addition/test_addition_mod_64_with_processed_trace.rs b/third_party/blake2b_halo2/src/tests/tests_addition/test_addition_mod_64_with_processed_trace.rs new file mode 100644 index 000000000..98aaa6685 --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/tests_addition/test_addition_mod_64_with_processed_trace.rs @@ -0,0 +1,153 @@ +use crate::tests::tests_addition::addition_mod_64_circuit_8bits::AdditionMod64Circuit8Bits; +use midnight_proofs::dev::MockProver; +use midnight_curves::bls12_381::Fq; +use rand::Rng; +use crate::tests::{generate_row_8bits, value_for, zero}; + +#[test] +fn test_positive_addition_with_0() { + // This value is used to assigned in cells where the value is not relevant for the circuit + // it is zero, but it could be any value + let unconstrained_value = zero(); + let trace = [ + [zero(), zero(), zero(), zero(), zero(), zero(), zero(), zero(), zero()], + [ + value_for(42u64), + zero(), + unconstrained_value, + unconstrained_value, + unconstrained_value, + unconstrained_value, + unconstrained_value, + unconstrained_value, + unconstrained_value, + ], + [ + value_for(42u64), + value_for(42u64), + zero(), + zero(), + zero(), + zero(), + zero(), + zero(), + zero(), + ], + ]; + let circuit = AdditionMod64Circuit8Bits::::new_for_trace(trace); + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); + + let trace = [ + [value_for(42u64), zero(), zero(), zero(), zero(), zero(), zero(), zero(), zero()], + [ + zero(), + zero(), + unconstrained_value, + unconstrained_value, + unconstrained_value, + unconstrained_value, + unconstrained_value, + unconstrained_value, + unconstrained_value, + ], + [ + value_for(42u64), + value_for(42u64), + zero(), + zero(), + zero(), + zero(), + zero(), + zero(), + zero(), + ], + ]; + let circuit = AdditionMod64Circuit8Bits::::new_for_trace(trace); + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); + + let trace = [generate_row_8bits::(0); 3]; + let circuit = AdditionMod64Circuit8Bits::::new_for_trace(trace); + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +#[test] +#[should_panic] +fn test_negative_addition() { + let trace = [ + generate_row_8bits::(1), + generate_row_8bits::(1), + generate_row_8bits::(3), + ]; + + let circuit = AdditionMod64Circuit8Bits::::new_for_trace(trace); + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +#[test] +#[should_panic] +fn test_negative_random_addition() { + let mut rng = rand::thread_rng(); + let trace = [ + generate_row_8bits::(rng.gen()), + generate_row_8bits::(rng.gen()), + generate_row_8bits::(rng.gen()), + ]; + + let circuit = AdditionMod64Circuit8Bits::::new_for_trace(trace); + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +#[test] +#[should_panic] +fn test_negative_sum_correct_but_no_carry_tracked() { + // This should panic because, although the sum is correct, the carry column is not computed. + // It should be 1. + let mut rng = rand::thread_rng(); + let x: u128 = rng.gen(); + let max_u64 = u64::MAX as u128; + let trace = [ + generate_row_8bits::(x), + generate_row_8bits::(max_u64), + generate_row_8bits::(x + max_u64), + ]; + let circuit = AdditionMod64Circuit8Bits::::new_for_trace(trace); + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +#[test] +#[should_panic] +fn test_negative_sum_correct_but_unnecessary_carry() { + // This should panic because, although the sum is correct, the carry column is incorrect. It should be 0. + let mut trace = [ + generate_row_8bits::(1), + generate_row_8bits::(2), + generate_row_8bits::(3), + ]; + trace[1][1] = value_for(1u8); + let circuit = AdditionMod64Circuit8Bits::::new_for_trace(trace); + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +#[test] +#[should_panic] +fn test_negative_sum_correct_but_decomposition_exceedes_range_check() { + // This should panic because, although the sum is correct, + // the result does not respect the max sizes + let mut trace = [ + generate_row_8bits::(1 << 8), + generate_row_8bits::((1 << 8) - 1), + generate_row_8bits::((1 << 9) - 1), + ]; + trace[0][1] = value_for(1u16 << 8); + trace[0][2] = value_for(0u8); + let circuit = AdditionMod64Circuit8Bits::::new_for_trace(trace); + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} diff --git a/third_party/blake2b_halo2/src/tests/tests_rotation/limb_rotation_circuit.rs b/third_party/blake2b_halo2/src/tests/tests_rotation/limb_rotation_circuit.rs new file mode 100644 index 000000000..9c1ccb4af --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/tests_rotation/limb_rotation_circuit.rs @@ -0,0 +1,119 @@ +use super::*; +use crate::tests::Decompose8Config; +use crate::base_operations::generic_limb_rotation::LimbRotation; +use ff::PrimeField; +use midnight_proofs::circuit::SimpleFloorPlanner; +use midnight_proofs::plonk::Circuit; +use std::array; + +#[derive(Clone)] +pub(crate) struct LimbRotationCircuit { + _ph: PhantomData, + trace: [[Value; 9]; 2], +} + +impl LimbRotationCircuit { + pub(crate) fn new_for_trace(trace: [[Value; 9]; 2]) -> Self { + Self { + _ph: PhantomData, + trace, + } + } +} + +impl Circuit for LimbRotationCircuit { + type Config = LimbRotationCircuitConfig; + type Params = (); + type FloorPlanner = SimpleFloorPlanner; + + fn without_witnesses(&self) -> Self { + Self { + _ph: PhantomData, + trace: [[Value::unknown(); 9]; 2], + } + } + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + let full_number_u64 = meta.advice_column(); + let limbs: [Column; 8] = array::from_fn(|_| { + let column = meta.advice_column(); + meta.enable_equality(column); + column + }); + + let decompose_8_config = Decompose8Config::configure(meta, full_number_u64, limbs); + + Self::Config { + _ph: PhantomData, + decompose_8_config: decompose_8_config.clone(), + limb_rotation_config: LimbRotation::configure(decompose_8_config.q_decompose), + } + } + + fn synthesize( + &self, + mut config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + let limbs_to_rotate_to_the_right = match T { + 32 => 4, + 24 => 3, + 16 => 2, + _ => panic!("Unexpected Rotation"), + }; + + config.decompose_8_config.populate_lookup_table(&mut layouter)?; + config.limb_rotation_config.populate_rotation_rows( + &mut layouter, + &mut config.decompose_8_config, + self.trace, + limbs_to_rotate_to_the_right, + ) + } +} + +impl LimbRotation { + /// This method is meant to receive a valid rotation_trace, and populate the circuit with it + /// The rotation trace is a 2x9 matrix. The rows represent the input and output of the rotation, + /// and the columns represent the limbs of each number. + /// In the end of the method, the circuit will have the correct constraints to ensure that + /// the output is the input rotated to the right by the number of limbs specified in the + /// limb_rotations_right parameter. + /// This method is not used in the actual circuit, but it is useful for testing if you want to + /// write a test where the values are incorrect and check that the contstaints fail. + fn populate_rotation_rows( + &self, + layouter: &mut impl Layouter, + decompose_config: &mut Decompose8Config, + trace: [[Value; 9]; 2], + limb_rotations_right: usize, + ) -> Result<(), Error> { + layouter.assign_region( + || format!("rotate {}", limb_rotations_right), + |mut region| { + let first_row = decompose_config.populate_row_from_values( + &mut region, + trace[0].as_ref(), + 0, + true, + )?; + let second_row = decompose_config.populate_row_from_values( + &mut region, + trace[1].as_ref(), + 1, + true, + )?; + + for i in 0..8 { + // We must subtract limb_rotations_right because if a number is expressed bitwise + // as x = l1|l2|...|l7|l8, the limbs are stored as [l8, l7, ..., l2, l1] + let top_cell = first_row[i + 1].cell(); + let bottom_cell = second_row[((8 + i - limb_rotations_right) % 8) + 1].cell(); + region.constrain_equal(top_cell, bottom_cell)?; + } + Ok(()) + }, + )?; + Ok(()) + } +} diff --git a/third_party/blake2b_halo2/src/tests/tests_rotation/limb_rotation_circuit_autogenerated.rs b/third_party/blake2b_halo2/src/tests/tests_rotation/limb_rotation_circuit_autogenerated.rs new file mode 100644 index 000000000..674392cab --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/tests_rotation/limb_rotation_circuit_autogenerated.rs @@ -0,0 +1,121 @@ +use super::*; +use crate::tests::Decompose8Config; +use crate::base_operations::generic_limb_rotation::LimbRotation; +use ff::PrimeField; +use midnight_proofs::circuit::SimpleFloorPlanner; +use midnight_proofs::plonk::{Circuit, Fixed}; +use std::array; + +#[derive(Clone)] +pub(crate) struct LimbRotationConfigAutogenerated { + limb_rotation_config: LimbRotationCircuitConfig, + fixed: Column, + full_number_u64: Column, + limbs: [Column; 8], +} + +#[derive(Clone)] +pub(crate) struct LimbRotationCircuitAutogenerated { + _ph: PhantomData, + input: Value, + result: Value, +} + +impl LimbRotationCircuitAutogenerated { + pub(crate) fn new_for(input: Value, expected_output: Value) -> Self { + Self { + _ph: PhantomData, + input, + result: expected_output, + } + } +} + +impl Circuit for LimbRotationCircuitAutogenerated { + type Config = LimbRotationConfigAutogenerated; + type Params = (); + type FloorPlanner = SimpleFloorPlanner; + + fn without_witnesses(&self) -> Self { + Self { + _ph: PhantomData, + input: Value::unknown(), + result: Value::unknown(), + } + } + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + let full_number_u64 = meta.advice_column(); + let limbs: [Column; 8] = array::from_fn(|_| { + let column = meta.advice_column(); + meta.enable_equality(column); + column + }); + let fixed = meta.fixed_column(); + meta.enable_equality(fixed); + meta.enable_equality(full_number_u64); + + let decompose_8_config = Decompose8Config::configure(meta, full_number_u64, limbs); + + Self::Config { + limb_rotation_config: LimbRotationCircuitConfig { + _ph: PhantomData, + decompose_8_config: decompose_8_config.clone(), + limb_rotation_config: LimbRotation::configure(decompose_8_config.q_decompose), + }, + fixed, + full_number_u64, + limbs, + } + } + + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + let limbs_to_rotate_to_the_right = match T { + 32 => 4, + 24 => 3, + 16 => 2, + _ => panic!("Unexpected Rotation"), + }; + + config.limb_rotation_config.decompose_8_config.populate_lookup_table(&mut layouter)?; + layouter.assign_region( + || format!("Rotate {} limbs", limbs_to_rotate_to_the_right), + |mut region| { + let mut offset = 0; + let input_row = config + .limb_rotation_config + .decompose_8_config + .generate_row_from_word_and_keep_row(&mut region, self.input, offset)?; + offset += 1; + + let result = config + .limb_rotation_config + .limb_rotation_config + .generate_rotation_rows_from_input_row( + &mut region, + &mut offset, + input_row, + limbs_to_rotate_to_the_right, + config.full_number_u64, + config.limbs, + )?; + + // Check that the calculation was performed correctly by the limb rotation config + let fixed_cell = region.assign_fixed( + || "assign expected result", + config.fixed, + 0, + || self.result, + )?; + region.constrain_equal(result.cell(), fixed_cell.cell())?; + Ok(()) + }, + )?; + + Ok(()) + } +} diff --git a/third_party/blake2b_halo2/src/tests/tests_rotation/mod.rs b/third_party/blake2b_halo2/src/tests/tests_rotation/mod.rs new file mode 100644 index 000000000..c5e65ccc5 --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/tests_rotation/mod.rs @@ -0,0 +1,59 @@ +use super::*; +use crate::tests::Decompose8Config; +use crate::base_operations::generic_limb_rotation::LimbRotation; +use crate::base_operations::rotate_63::Rotate63Config; + +// -------- Rotation 63 with 8 bit limbs ------------ +mod rotation_63_circuit_8bit_limbs; +#[cfg(test)] +mod test_rotation_63_8_bit_limbs; + +mod rotation_63_cirtuit_8bit_limbs_autogenerated; +#[cfg(test)] +mod test_rotation_63_8_bit_limbs_autogenerated; + +#[derive(Clone)] +pub(crate) struct Rotation63Config8bitLimbs { + _ph: PhantomData, + rotation_63_config: Rotate63Config, + decompose_8_config: Decompose8Config, +} + +// -------------------------------------------------- + +#[cfg(test)] +mod limb_rotation_circuit; +mod test_limb_rotation_16_24_32; + +mod limb_rotation_circuit_autogenerated; +#[cfg(test)] +mod test_limb_rotation_16_24_32_autogenerated; + +#[derive(Clone)] +pub(crate) struct LimbRotationCircuitConfig { + _ph: PhantomData, + decompose_8_config: Decompose8Config, + limb_rotation_config: LimbRotation, +} + +impl Rotate63Config { + /// Receives a trace and populates the rows for the rotation of 63 bits to the right + fn populate_rotation_rows( + &self, + layouter: &mut impl Layouter, + decompose_config: &mut Decompose8Config, + trace: [[Value; 9]; 2], + ) -> Result<(), Error> { + layouter.assign_region( + || "rotate 63", + |mut region| { + let first_row = trace[0].to_vec(); + let second_row = trace[1].to_vec(); + decompose_config.populate_row_from_values(&mut region, &first_row, 0, true)?; + decompose_config.populate_row_from_values(&mut region, &second_row, 1, true)?; + self.q_rot63.enable(&mut region, 1) + }, + )?; + Ok(()) + } +} diff --git a/third_party/blake2b_halo2/src/tests/tests_rotation/rotation_63_circuit_8bit_limbs.rs b/third_party/blake2b_halo2/src/tests/tests_rotation/rotation_63_circuit_8bit_limbs.rs new file mode 100644 index 000000000..18e7709ec --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/tests_rotation/rotation_63_circuit_8bit_limbs.rs @@ -0,0 +1,67 @@ +use super::*; +use crate::tests::Decompose8Config; +use crate::base_operations::rotate_63::Rotate63Config; +use midnight_proofs::circuit::SimpleFloorPlanner; +use midnight_proofs::plonk::Circuit; +use std::array; +use std::marker::PhantomData; + +pub(crate) struct Rotation63Circuit8bitLimbs { + _ph: PhantomData, + trace: [[Value; 9]; 2], +} + +impl Rotation63Circuit8bitLimbs { + pub(crate) fn new_for_trace(trace: [[Value; 9]; 2]) -> Self { + Self { + _ph: PhantomData, + trace, + } + } +} + +impl Circuit for Rotation63Circuit8bitLimbs { + type Config = Rotation63Config8bitLimbs; + type Params = (); + type FloorPlanner = SimpleFloorPlanner; + + fn without_witnesses(&self) -> Self { + Self { + _ph: PhantomData, + trace: [[Value::unknown(); 9]; 2], + } + } + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + let full_number_u64 = meta.advice_column(); + let limbs_8_bits: [Column; 8] = array::from_fn(|_| meta.advice_column()); + + let decompose_8_config = Decompose8Config::configure(meta, full_number_u64, limbs_8_bits); + let rotation_63_config = Rotate63Config::configure( + meta, + full_number_u64, + decompose_8_config.q_decompose, + decompose_8_config.q_range, + ); + + Self::Config { + _ph: PhantomData, + decompose_8_config, + rotation_63_config, + } + } + + #[allow(unused_variables)] + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + config.decompose_8_config.populate_lookup_table(&mut layouter)?; + config.rotation_63_config.populate_rotation_rows( + &mut layouter, + &mut config.decompose_8_config.clone(), + self.trace, + ) + } +} diff --git a/third_party/blake2b_halo2/src/tests/tests_rotation/rotation_63_cirtuit_8bit_limbs_autogenerated.rs b/third_party/blake2b_halo2/src/tests/tests_rotation/rotation_63_cirtuit_8bit_limbs_autogenerated.rs new file mode 100644 index 000000000..32707a94d --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/tests_rotation/rotation_63_cirtuit_8bit_limbs_autogenerated.rs @@ -0,0 +1,118 @@ +use super::*; +use crate::tests::Decompose8Config; +use crate::base_operations::rotate_63::Rotate63Config; +use midnight_proofs::circuit::SimpleFloorPlanner; +use midnight_proofs::plonk::{Circuit, Fixed}; +use std::array; +use std::marker::PhantomData; + +#[derive(Clone)] +pub(crate) struct Rotation63Config8bitWithResultValidation { + _ph: PhantomData, + rotation_63_config: Rotation63Config8bitLimbs, + fixed: Column, + full_number_u64: Column, + limbs: [Column; 8], +} + +pub(crate) struct Rotation63Circuit8bitLimbsAutogenerated { + _ph: PhantomData, + input: Value, + result: Value, +} + +impl Rotation63Circuit8bitLimbsAutogenerated { + pub(crate) fn new_for(input: Value, result: Value) -> Self { + Self { + _ph: PhantomData, + input, + result, + } + } +} + +impl Circuit for Rotation63Circuit8bitLimbsAutogenerated { + type Config = Rotation63Config8bitWithResultValidation; + type Params = (); + type FloorPlanner = SimpleFloorPlanner; + + fn without_witnesses(&self) -> Self { + Self { + _ph: PhantomData, + input: Value::unknown(), + result: Value::unknown(), + } + } + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + let full_number_u64 = meta.advice_column(); + let fixed = meta.fixed_column(); + meta.enable_equality(full_number_u64); + meta.enable_equality(fixed); + + let limbs: [Column; 8] = array::from_fn(|_| meta.advice_column()); + + let decompose_8_chip = Decompose8Config::configure(meta, full_number_u64, limbs); + + let rotation_63_chip = Rotate63Config::configure( + meta, + full_number_u64, + decompose_8_chip.q_decompose, + decompose_8_chip.q_range, + ); + + Self::Config { + _ph: PhantomData, + rotation_63_config: Rotation63Config8bitLimbs { + _ph: PhantomData, + decompose_8_config: decompose_8_chip, + rotation_63_config: rotation_63_chip, + }, + fixed, + full_number_u64, + limbs, + } + } + + #[allow(unused_variables)] + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + config.rotation_63_config.decompose_8_config.populate_lookup_table(&mut layouter)?; + layouter.assign_region( + || "generate rotate 63", + |mut region| { + let mut offset = 0; + let a_row = config + .rotation_63_config + .decompose_8_config + .generate_row_from_word_and_keep_row(&mut region, self.input, offset)?; + offset += 1; + let result = config + .rotation_63_config + .rotation_63_config + .generate_64_bit_rotation_from_cells( + &mut region, + &mut offset, + &a_row.full_number.clone(), + config.full_number_u64, + config.limbs, + )?; + + // Check that the calculation was performed correctly by the Rotate63 chip + let fixed_cell = region.assign_fixed( + || "assign expected result", + config.fixed, + 0, + || self.result, + )?; + // Constrain expected result + region.constrain_equal(result.cell(), fixed_cell.cell())?; + Ok(()) + }, + )?; + Ok(()) + } +} diff --git a/third_party/blake2b_halo2/src/tests/tests_rotation/test_limb_rotation_16_24_32.rs b/third_party/blake2b_halo2/src/tests/tests_rotation/test_limb_rotation_16_24_32.rs new file mode 100644 index 000000000..5fcae28c1 --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/tests_rotation/test_limb_rotation_16_24_32.rs @@ -0,0 +1,180 @@ +use super::*; +use crate::tests::tests_rotation::limb_rotation_circuit::LimbRotationCircuit; +use midnight_proofs::dev::MockProver; +use rand::Rng; + +// ------------ ROTATION 32 ------------ // + +#[test] +fn test_positive_rotate_right_32() { + let first_row: [Value; 9] = + generate_row_8bits((1u64 << 32) - 1u64)[0..9].try_into().unwrap(); + let second_row: [Value; 9] = + generate_row_8bits((1u128 << 64) - (1u128 << 32))[0..9].try_into().unwrap(); + let valid_rotation_32_trace = [first_row, second_row]; + + let circuit = LimbRotationCircuit::::new_for_trace(valid_rotation_32_trace); + + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +#[test] +fn test_positive_random_rotate_right_32() { + let mut rng = rand::thread_rng(); + let n: u64 = rng.gen(); + let pow32 = 1u64 << 32; + let expected_result = ((n % pow32) << 32) + (n / pow32); + let first_row: [Value; 9] = generate_row_8bits(n)[0..9].try_into().unwrap(); + let second_row: [Value; 9] = generate_row_8bits(expected_result)[0..9].try_into().unwrap(); + let valid_rotation_32_trace = [first_row, second_row]; + + let circuit = LimbRotationCircuit::::new_for_trace(valid_rotation_32_trace); + + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +#[test] +#[should_panic] +fn test_negative_rotate_right_32() { + let first_row: [Value; 9] = + generate_row_8bits((1u64 << 32) - 1u64)[0..9].try_into().unwrap(); + let second_row: [Value; 9] = + generate_row_8bits((1u128 << 64) - 1)[0..9].try_into().unwrap(); + let invalid_rotation_32_trace = [first_row, second_row]; + + let circuit = LimbRotationCircuit::::new_for_trace(invalid_rotation_32_trace); + + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +#[test] +#[should_panic] +fn test_negative_correct_rotation32_wrong_decomposition() { + let mut first_row: [Value; 9] = generate_row_8bits(1u64 << 32)[0..9].try_into().unwrap(); + let second_row: [Value; 9] = generate_row_8bits(1u64)[0..9].try_into().unwrap(); + first_row[4] = value_for(0u8); + first_row[3] = value_for(1u16 << 8); + let invalid_rotation_32_trace = [first_row, second_row]; + + let circuit = LimbRotationCircuit::::new_for_trace(invalid_rotation_32_trace); + + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +// ------------ ROTATION 24 ------------ // +#[test] +fn test_positive_rotate_right_24_limbs() { + let first_row: [Value; 9] = generate_row_8bits(1u128 << 24)[0..9].try_into().unwrap(); + let second_row: [Value; 9] = generate_row_8bits(1u128)[0..9].try_into().unwrap(); + let valid_rotation_trace = [first_row, second_row]; + + let circuit = LimbRotationCircuit::::new_for_trace(valid_rotation_trace); + + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +#[test] +fn test_positive_random_rotate_right_24() { + let mut rng = rand::thread_rng(); + let n: u64 = rng.gen(); + let pow24 = 1u64 << 24; + let expected_result = ((n % pow24) << 40) + (n / pow24); + let first_row: [Value; 9] = generate_row_8bits(n)[0..9].try_into().unwrap(); + let second_row: [Value; 9] = generate_row_8bits(expected_result)[0..9].try_into().unwrap(); + let valid_rotation_24_trace = [first_row, second_row]; + + let circuit = LimbRotationCircuit::::new_for_trace(valid_rotation_24_trace); + + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +#[test] +#[should_panic] +fn test_negative_rotate_right_24_limbs() { + let first_row: [Value; 9] = generate_row_8bits(1u128 << 24)[0..9].try_into().unwrap(); + let second_row: [Value; 9] = generate_row_8bits(2u8)[0..9].try_into().unwrap(); + let valid_rotation_trace = [first_row, second_row]; + + let circuit = LimbRotationCircuit::::new_for_trace(valid_rotation_trace); + + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +#[test] +#[should_panic] +fn test_negative_correct_rotation24_wrong_decomposition() { + let mut first_row: [Value; 9] = generate_row_8bits(1u64 << 32)[0..9].try_into().unwrap(); + let second_row: [Value; 9] = generate_row_8bits(1u64 << 8)[0..9].try_into().unwrap(); + first_row[4] = value_for(0u8); + first_row[3] = value_for(1u16 << 8); + let invalid_rotation_24_trace = [first_row, second_row]; + + let circuit = LimbRotationCircuit::::new_for_trace(invalid_rotation_24_trace); + + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +// ------------ ROTATION 16 ------------ // +#[test] +fn test_positive_rotate_right_16_limbs() { + let first_row: [Value; 9] = generate_row_8bits(1u128 << 16)[0..9].try_into().unwrap(); + let second_row: [Value; 9] = generate_row_8bits(1u128)[0..9].try_into().unwrap(); + let valid_rotation_trace = [first_row, second_row]; + + let circuit = LimbRotationCircuit::::new_for_trace(valid_rotation_trace); + + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +#[test] +fn test_positive_random_rotate_right_16() { + let mut rng = rand::thread_rng(); + let n: u64 = rng.gen(); + let pow16 = 1u64 << 16; + let expected_result = ((n % pow16) << 48) + (n / pow16); + let first_row: [Value; 9] = generate_row_8bits(n)[0..9].try_into().unwrap(); + let second_row: [Value; 9] = generate_row_8bits(expected_result)[0..9].try_into().unwrap(); + let valid_rotation_16_trace = [first_row, second_row]; + + let circuit = LimbRotationCircuit::::new_for_trace(valid_rotation_16_trace); + + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +#[test] +#[should_panic] +fn test_negative_rotate_right_16_limbs() { + let first_row: [Value; 9] = generate_row_8bits(1u128 << 16)[0..9].try_into().unwrap(); + let second_row: [Value; 9] = generate_row_8bits(2u8)[0..9].try_into().unwrap(); + let valid_rotation_trace = [first_row, second_row]; + + let circuit = LimbRotationCircuit::::new_for_trace(valid_rotation_trace); + + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +#[test] +#[should_panic] +fn test_negative_correct_rotation16_wrong_decomposition() { + let mut first_row: [Value; 9] = generate_row_8bits(1u64 << 32)[0..9].try_into().unwrap(); + let second_row: [Value; 9] = generate_row_8bits(1u64 << 16)[0..9].try_into().unwrap(); + first_row[4] = value_for(0u8); + first_row[3] = value_for(1u16 << 8); + let invalid_rotation_16_trace = [first_row, second_row]; + + let circuit = LimbRotationCircuit::::new_for_trace(invalid_rotation_16_trace); + + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} diff --git a/third_party/blake2b_halo2/src/tests/tests_rotation/test_limb_rotation_16_24_32_autogenerated.rs b/third_party/blake2b_halo2/src/tests/tests_rotation/test_limb_rotation_16_24_32_autogenerated.rs new file mode 100644 index 000000000..f4c543741 --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/tests_rotation/test_limb_rotation_16_24_32_autogenerated.rs @@ -0,0 +1,66 @@ +use super::*; +use crate::tests::tests_rotation::limb_rotation_circuit_autogenerated::LimbRotationCircuitAutogenerated; +use midnight_proofs::dev::MockProver; + +#[test] +fn test_limb_rotation_chip_should_generate_32_bit_rotation_trace_correctly() { + let input_number = 1u128; + let expected_result_number = input_number << 32; + + run_test_limb_rotation_autogenerated::<32>(input_number, expected_result_number); +} + +#[test] +#[should_panic] +fn test_limb_rotation_chip_for_32_bits_should_panic_over_a_wrong_result() { + let input_number = 1u128; + let wrong_expected_result_number = (input_number << 32) + 1; + + run_test_limb_rotation_autogenerated::<32>(input_number, wrong_expected_result_number); +} + +#[test] +fn test_limb_rotation_chip_should_generate_24_bit_rotation_trace_correctly() { + let expected_result_number = 1u128; + let input_number = expected_result_number << 24; + + run_test_limb_rotation_autogenerated::<24>(input_number, expected_result_number); +} + +#[test] +#[should_panic] +fn test_limb_rotation_chip_for_24_bits_should_panic_over_a_wrong_result() { + let input_number = 1u128; + let wrong_expected_result_number = (input_number << 24) + 1; + + run_test_limb_rotation_autogenerated::<24>(input_number, wrong_expected_result_number); +} + +#[test] +fn test_limb_rotation_chip_should_generate_16_bit_rotation_trace_correctly() { + let input_number = 1u128 << 16; + let expected_result_number = 1u128; + + run_test_limb_rotation_autogenerated::<16>(input_number, expected_result_number); +} + +#[test] +#[should_panic] +fn test_limb_rotation_chip_for_16_bits_should_panic_over_a_wrong_result() { + let input_number = 1u128; + let wrong_expected_result_number = (input_number << 16) + 1; + + run_test_limb_rotation_autogenerated::<16>(input_number, wrong_expected_result_number); +} + +fn run_test_limb_rotation_autogenerated( + input_number: u128, + expected_result_number: u128, +) { + let circuit = LimbRotationCircuitAutogenerated::::new_for( + Value::known(Blake2bWord(input_number as u64)), + Value::known(Blake2bWord(expected_result_number as u64)), + ); + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} diff --git a/third_party/blake2b_halo2/src/tests/tests_rotation/test_rotation_63_8_bit_limbs.rs b/third_party/blake2b_halo2/src/tests/tests_rotation/test_rotation_63_8_bit_limbs.rs new file mode 100644 index 000000000..3705133d7 --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/tests_rotation/test_rotation_63_8_bit_limbs.rs @@ -0,0 +1,40 @@ +use super::*; +use crate::tests::tests_rotation::rotation_63_circuit_8bit_limbs::Rotation63Circuit8bitLimbs; +use midnight_proofs::dev::MockProver; + +#[test] +fn test_positive_rotate_right_63() { + let circuit = Rotation63Circuit8bitLimbs::::new_for_trace(valid_rotation_63_trace_8bit()); + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +#[test] +#[should_panic] +fn test_negative_rotate_right_63() { + let mut invalid_rotation_trace = valid_rotation_63_trace_8bit(); + invalid_rotation_trace[0][1] = one() + invalid_rotation_trace[0][1]; + invalid_rotation_trace[0][0] = one() + invalid_rotation_trace[0][0]; + + let circuit = Rotation63Circuit8bitLimbs::::new_for_trace(invalid_rotation_trace); + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +#[test] +#[should_panic] +fn test_badly_decomposed_rotate_right_63() { + let mut invalid_rotation_trace = valid_rotation_63_trace_8bit(); + invalid_rotation_trace[1][2] = one(); + + let circuit = Rotation63Circuit8bitLimbs::::new_for_trace(invalid_rotation_trace); + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +fn valid_rotation_63_trace_8bit() -> [[Value; 9]; 2] { + [ + [one(), one(), zero(), zero(), zero(), zero(), zero(), zero(), zero()], + [one() + one(), one() + one(), zero(), zero(), zero(), zero(), zero(), zero(), zero()], + ] +} diff --git a/third_party/blake2b_halo2/src/tests/tests_rotation/test_rotation_63_8_bit_limbs_autogenerated.rs b/third_party/blake2b_halo2/src/tests/tests_rotation/test_rotation_63_8_bit_limbs_autogenerated.rs new file mode 100644 index 000000000..9d43e6b44 --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/tests_rotation/test_rotation_63_8_bit_limbs_autogenerated.rs @@ -0,0 +1,24 @@ +use super::*; +use crate::tests::tests_rotation::rotation_63_cirtuit_8bit_limbs_autogenerated::Rotation63Circuit8bitLimbsAutogenerated; +use midnight_proofs::dev::MockProver; + +#[test] +fn test_rot_63_chip_should_know_how_to_create_trace_from_inputs() { + let circuit = Rotation63Circuit8bitLimbsAutogenerated::::new_for( + Value::known(Blake2bWord(1u8.into())), + Value::known(Blake2bWord(2u8.into())), + ); + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + assert_eq!(prover.verify(), Ok(())); +} + +#[test] +#[should_panic] +fn test_rot_63_chip_should_panic_over_a_wrong_result() { + let circuit = Rotation63Circuit8bitLimbsAutogenerated::::new_for( + Value::known(Blake2bWord(1u8.into())), + Value::known(Blake2bWord(3u8.into())), + ); + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + assert_eq!(prover.verify(), Ok(())); +} diff --git a/third_party/blake2b_halo2/src/tests/tests_xor/mod.rs b/third_party/blake2b_halo2/src/tests/tests_xor/mod.rs new file mode 100644 index 000000000..bb95191ba --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/tests_xor/mod.rs @@ -0,0 +1,6 @@ +use super::*; + +mod tests_xor_with_generated_trace; +mod tests_xor_with_processed_trace; +mod xor_circuit; +mod xor_circuit_autogenerated; diff --git a/third_party/blake2b_halo2/src/tests/tests_xor/tests_xor_with_generated_trace.rs b/third_party/blake2b_halo2/src/tests/tests_xor/tests_xor_with_generated_trace.rs new file mode 100644 index 000000000..3140d34be --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/tests_xor/tests_xor_with_generated_trace.rs @@ -0,0 +1,30 @@ +use super::*; +use crate::tests::tests_xor::xor_circuit_autogenerated::XorCircuitAutogenerated; +use midnight_proofs::dev::MockProver; + +#[test] +fn test_positive_xor() { + let a = ((1u128 << 64) - 1) as u64; + + test_xor_operation(a, a, a ^ a); +} + +#[test] +#[should_panic] +fn test_negative_xor() { + let a = ((1u128 << 64) - 1) as u64; + let b = 0; + + test_xor_operation(a, b, b); +} + +fn test_xor_operation(a: u64, b: u64, result: u64) { + let circuit = XorCircuitAutogenerated::::new_for( + blake2b_value_for(a), + blake2b_value_for(b), + blake2b_value_for(result), + ); + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + + prover.verify().unwrap(); +} diff --git a/third_party/blake2b_halo2/src/tests/tests_xor/tests_xor_with_processed_trace.rs b/third_party/blake2b_halo2/src/tests/tests_xor/tests_xor_with_processed_trace.rs new file mode 100644 index 000000000..aca9abde1 --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/tests_xor/tests_xor_with_processed_trace.rs @@ -0,0 +1,149 @@ +use super::*; +use crate::tests::tests_xor::xor_circuit::XorCircuit; +use midnight_proofs::dev::MockProver; +use rand::Rng; + +#[test] +fn test_positive_random_duplicate_xor() { + let mut rng = rand::thread_rng(); + let n: u64 = rng.gen(); + let valid_xor_trace: [[Value; 9]; 3] = [ + row_decomposed_in_8_limbs_from_u64(n), // a + row_decomposed_in_8_limbs_from_u64(n), // b + row_decomposed_in_8_limbs_from_u64(0), // a xor b + ]; + + let circuit = XorCircuit::::new_for_trace(valid_xor_trace); + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + + prover.verify().unwrap(); +} + +#[test] +fn test_positive_random_xor_with_0() { + let mut rng = rand::thread_rng(); + let n: u64 = rng.gen(); + let valid_xor_trace: [[Value; 9]; 3] = [ + row_decomposed_in_8_limbs_from_u64(n), + row_decomposed_in_8_limbs_from_u64(0), + row_decomposed_in_8_limbs_from_u64(n), + ]; + + let circuit = XorCircuit::::new_for_trace(valid_xor_trace); + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + + prover.verify().unwrap(); +} + +#[test] +fn test_positive_random_xor() { + let mut rng = rand::thread_rng(); + let n1: u64 = rng.gen(); + let n2: u64 = rng.gen(); + let valid_xor_trace: [[Value; 9]; 3] = [ + row_decomposed_in_8_limbs_from_u64(n1), // a + row_decomposed_in_8_limbs_from_u64(n2), // b + row_decomposed_in_8_limbs_from_u64(n1 ^ n2), // a xor b + ]; + + let circuit = XorCircuit::::new_for_trace(valid_xor_trace); + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + + prover.verify().unwrap(); +} + +#[test] +#[should_panic] +fn test_xor_badly_done() { + let incorrect_xor_trace: [[Value; 9]; 3] = [ + row_decomposed_in_8_limbs_from_u64(((1u128 << 64) - 1) as u64), // a + row_decomposed_in_8_limbs_from_u64(((1u128 << 64) - 2) as u64), // b + row_decomposed_in_8_limbs_from_u64(0), // a xor b + ]; + + let circuit = XorCircuit::::new_for_trace(incorrect_xor_trace); + + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +#[test] +#[should_panic] +fn test_incorrect_xor_in_fifth_limb() { + let incorrect_xor_trace: [[Value; 9]; 3] = [ + row_decomposed_in_8_limbs_from_u64(1u64 << 33), // a + row_decomposed_in_8_limbs_from_u64((1u64 << 33) + (1u64 << 34)), // b + row_decomposed_in_8_limbs_from_u64(0), // a xor b + ]; + + let circuit = XorCircuit::::new_for_trace(incorrect_xor_trace); + + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +#[test] +#[should_panic] +fn test_bad_decomposition_in_8_bit_limbs() { + let mut badly_decomposed_row = [value_for(0u16); 9]; + badly_decomposed_row[4] = value_for(1u16); + + let badly_decomposed_xor_trace: [[Value; 9]; 3] = [ + row_decomposed_in_8_limbs_from_u64(((1u128 << 64) - 1) as u64), // a + row_decomposed_in_8_limbs_from_u64(((1u128 << 64) - 1) as u64), // b + badly_decomposed_row, // a xor b + ]; + + let circuit = XorCircuit::::new_for_trace(badly_decomposed_xor_trace); + + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +#[test] +#[should_panic] +fn test_bad_range_check_limb_u8() { + let out_of_range_decomposition_row = [ + value_for((1u32 << 16) - 1), + value_for((1u32 << 16) - 1), + value_for(0u16), + value_for(0u16), + value_for(0u16), + value_for(0u16), + value_for(0u16), + value_for(0u16), + value_for(0u16), + ]; + + let badly_decomposed_xor_trace: [[Value; 9]; 3] = [ + out_of_range_decomposition_row, + row_decomposed_in_8_limbs_from_u64(0u64), // b + out_of_range_decomposition_row, // a xor b + ]; + + let circuit = XorCircuit::::new_for_trace(badly_decomposed_xor_trace); + + let prover = MockProver::run(17, &circuit, vec![]).unwrap(); + prover.verify().unwrap(); +} + +fn row_decomposed_in_8_limbs_from_u64(x: u64) -> [Value; 9] { + let mut x_aux = x; + let mut limbs: [u64; 8] = [0; 8]; + for limb in limbs.iter_mut() { + *limb = x_aux % 256; + x_aux /= 256; + } + + [ + value_for(x), + value_for(limbs[0]), + value_for(limbs[1]), + value_for(limbs[2]), + value_for(limbs[3]), + value_for(limbs[4]), + value_for(limbs[5]), + value_for(limbs[6]), + value_for(limbs[7]), + ] +} diff --git a/third_party/blake2b_halo2/src/tests/tests_xor/xor_circuit.rs b/third_party/blake2b_halo2/src/tests/tests_xor/xor_circuit.rs new file mode 100644 index 000000000..3cc63d050 --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/tests_xor/xor_circuit.rs @@ -0,0 +1,106 @@ +use super::*; +use crate::tests::Decompose8Config; +use crate::base_operations::xor::XorConfig; +use midnight_proofs::circuit::SimpleFloorPlanner; +use midnight_proofs::plonk::Circuit; +use std::array; +use std::marker::PhantomData; + +#[derive(Clone)] +pub(crate) struct XorCircuitConfig { + _ph: PhantomData, + xor_config: XorConfig, + decompose_8_config: Decompose8Config, +} + +pub(crate) struct XorCircuit { + _ph: PhantomData, + trace: [[Value; 9]; 3], +} + +impl XorCircuit { + pub(crate) fn new_for_trace(trace: [[Value; 9]; 3]) -> Self { + Self { + _ph: PhantomData, + trace, + } + } +} + +impl Circuit for XorCircuit { + type Config = XorCircuitConfig; + type Params = (); + type FloorPlanner = SimpleFloorPlanner; + + fn without_witnesses(&self) -> Self { + Self { + _ph: PhantomData, + trace: [[Value::unknown(); 9]; 3], + } + } + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + let full_number_u64 = meta.advice_column(); + let limbs: [Column; 8] = array::from_fn(|_| meta.advice_column()); + + let decompose_8_config = Decompose8Config::configure(meta, full_number_u64, limbs); + let xor_config = XorConfig::configure( + meta, + limbs, + full_number_u64, + limbs, + decompose_8_config.q_decompose, + ); + + Self::Config { + _ph: PhantomData, + xor_config, + decompose_8_config, + } + } + + #[allow(unused_variables)] + fn synthesize( + &self, + mut config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + config.decompose_8_config.populate_lookup_table(&mut layouter)?; + + config.xor_config.populate_xor_lookup_table(&mut layouter)?; + config.xor_config.populate_xor_region( + &mut layouter, + self.trace, + &mut config.decompose_8_config, + ) + } +} + +impl XorConfig { + /// Given 3 explicit rows of values, it assigns the full number and the limbs of the operands + /// and the result in the trace + fn populate_xor_region( + &self, + layouter: &mut impl Layouter, + xor_trace: [[Value; 9]; 3], + decompose_8_config: &mut Decompose8Config, + ) -> Result<(), Error> { + layouter.assign_region( + || "xor", + |mut region| { + self.q_xor.enable(&mut region, 0)?; + + let first_row = xor_trace[0].to_vec(); + let second_row = xor_trace[1].to_vec(); + let third_row = xor_trace[2].to_vec(); + + decompose_8_config.populate_row_from_values(&mut region, &first_row, 0, true)?; + decompose_8_config.populate_row_from_values(&mut region, &second_row, 1, true)?; + decompose_8_config.populate_row_from_values(&mut region, &third_row, 2, true)?; + + Ok(()) + }, + )?; + Ok(()) + } +} diff --git a/third_party/blake2b_halo2/src/tests/tests_xor/xor_circuit_autogenerated.rs b/third_party/blake2b_halo2/src/tests/tests_xor/xor_circuit_autogenerated.rs new file mode 100644 index 000000000..2b6dd4657 --- /dev/null +++ b/third_party/blake2b_halo2/src/tests/tests_xor/xor_circuit_autogenerated.rs @@ -0,0 +1,136 @@ +use super::*; +use crate::tests::Decompose8Config; +use midnight_proofs::circuit::SimpleFloorPlanner; +use midnight_proofs::plonk::{Circuit, Fixed}; +use std::array; +use std::marker::PhantomData; +use crate::types::blake2b_word::Blake2bWord; + +use crate::base_operations::xor::XorConfig; + +#[derive(Clone)] +pub(crate) struct XorCircuitConfig { + _ph: PhantomData, + xor_config: XorConfig, + decompose_8_config: Decompose8Config, +} + +pub(crate) struct XorCircuitAutogenerated { + _ph: PhantomData, + value_a: Value, + value_b: Value, + expected_result: Value, +} + +#[derive(Clone)] +pub(crate) struct XorConfigWithResultValidation { + _ph: PhantomData, + xor_config: XorCircuitConfig, + fixed: Column, +} + +impl XorCircuitAutogenerated { + pub(crate) fn new_for( + value_a: Value, + value_b: Value, + expected_result: Value, + ) -> Self { + Self { + _ph: PhantomData, + value_a, + value_b, + expected_result, + } + } +} + +impl Circuit for XorCircuitAutogenerated { + type Config = XorConfigWithResultValidation; + type Params = (); + type FloorPlanner = SimpleFloorPlanner; + + fn without_witnesses(&self) -> Self { + Self { + _ph: PhantomData, + value_a: Value::unknown(), + value_b: Value::unknown(), + expected_result: Value::unknown(), + } + } + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + let full_number_u64 = meta.advice_column(); + let limbs: [Column; 8] = array::from_fn(|_| meta.advice_column()); + + let decompose_8_config = Decompose8Config::configure(meta, full_number_u64, limbs); + + let xor_config = XorConfig::configure( + meta, + limbs, + full_number_u64, + limbs, + decompose_8_config.q_decompose, + ); + + let fixed = meta.fixed_column(); + meta.enable_equality(full_number_u64); + meta.enable_equality(fixed); + + Self::Config { + _ph: PhantomData, + xor_config: XorCircuitConfig { + _ph: PhantomData, + decompose_8_config, + xor_config, + }, + fixed, + } + } + + #[allow(unused_variables)] + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + config.xor_config.decompose_8_config.populate_lookup_table(&mut layouter)?; + config.xor_config.xor_config.populate_xor_lookup_table(&mut layouter)?; + layouter.assign_region( + || "xor", + |mut region| { + let mut offset = 0; + let a_cell = config + .xor_config + .decompose_8_config + .generate_row_from_word_and_keep_row(&mut region, self.value_a, offset)? + .full_number; + offset += 1; + let b_cell = config + .xor_config + .decompose_8_config + .generate_row_from_word_and_keep_row(&mut region, self.value_b, offset)? + .full_number; + offset += 1; + + let result = config + .xor_config + .xor_config + .generate_xor_rows_from_cells(&mut region, &mut offset, &a_cell, &b_cell)? + .full_number + .clone(); + + let fixed_cell = region.assign_fixed( + || "assign fixed", + config.fixed, + 0, + || self.expected_result, + )?; + region.constrain_equal(result.cell(), fixed_cell.cell())?; + + Ok(()) + }, + )?; + + Ok(()) + } +} diff --git a/third_party/blake2b_halo2/src/types/bit.rs b/third_party/blake2b_halo2/src/types/bit.rs new file mode 100644 index 000000000..6446e8a83 --- /dev/null +++ b/third_party/blake2b_halo2/src/types/bit.rs @@ -0,0 +1,66 @@ +use super::*; +use ff::PrimeField; +use midnight_proofs::circuit::{Region, Value}; +use midnight_proofs::plonk::{Advice, Column, Error}; +use midnight_proofs::utils::rational::Rational; + +/// The inner type of AssignedBit. A wrapper around `bool` +#[derive(Copy, Clone, Debug)] +struct Bit(bool); + +impl Bit { + /// Creates a new [Bit] element. When the byte is created, it is constrained to be in the + /// range [0, 1] and its internal member is a boolean. + fn new_from_field(field: F) -> Self { + let bi_v = get_word_biguint_from_le_field(field); + #[cfg(not(test))] + assert!(bi_v == BigUint::from(0u8) || bi_v == BigUint::from(1u8)); + let bit = bi_v.to_bytes_le().first().copied().unwrap(); + Bit(bit == 1) + } +} + +/// Allows us to call the .assign_advice() method of the region with a Bit as its value +impl From<&Bit> for Rational { + fn from(value: &Bit) -> Self { + Self::Trivial(F::from(value.0 as u64)) + } +} + +/// This wrapper type on `AssignedNative` is designed to enforce type safety +/// on assigned bits. It is used in the addition chip to enforce that the +/// carry value is 0 or 1 +#[must_use] +pub(crate) struct AssignedBit(#[allow(dead_code)] AssignedCell); + +impl AssignedBit { + /// This method assigns a bit in the trace. The bit is range-checked both in + /// synthesize time and constrained in the circuit. The idea is that only the base operations + /// can create an [AssignedBit] from a Field value, since they're responsible to activate the + /// constraints over the cells in the trace. In this case, the AdditionMod64 gate is the + /// responsible to create constraints over the carry bit, which will be represented by an + /// [AssignedBit]. + pub(crate) fn assign_advice_bit( + region: &mut Region<'_, F>, + annotation: &str, + column: Column, + offset: usize, + value: Value, + ) -> Result { + // Check value is in range + let bit_value = value.map(|v| Bit::new_from_field(v)); + // Create AssignedCell with the same value but different type + let assigned_bit = + Self(region.assign_advice(|| annotation, column, offset, || bit_value)?); + Ok(assigned_bit) + } +} + +#[cfg(test)] +use midnight_proofs::circuit::Cell; +#[cfg(test)] +impl AssignedBit { + pub(crate) fn cell(&self) -> Cell { + self.0.cell() + } +} diff --git a/third_party/blake2b_halo2/src/types/blake2b_word.rs b/third_party/blake2b_halo2/src/types/blake2b_word.rs new file mode 100644 index 000000000..e67f2c755 --- /dev/null +++ b/third_party/blake2b_halo2/src/types/blake2b_word.rs @@ -0,0 +1,134 @@ +use super::*; +use ff::PrimeField; +use midnight_proofs::circuit::{AssignedCell, Cell, Region, Value}; +use midnight_proofs::plonk::{Advice, Column, Error}; +use midnight_proofs::utils::rational::Rational; +use std::ops::{BitXor, Sub}; + +/// The inner type of AssignedBlake2bWord. A wrapper around `u64` +#[derive(Copy, Clone, Debug)] +pub(crate) struct Blake2bWord(pub u64); + +impl Blake2bWord { + /// Creates a new [Blake2bWord] element. When the Blake2bWord is created, it is constrained to be in the + /// range [0, 2^64 - 1]. + pub(crate) fn new_from_field(field: F) -> Self { + let bi_v = get_word_biguint_from_le_field(field); + #[cfg(not(test))] + assert!(bi_v <= BigUint::from((1u128 << 64) - 1)); + let mut bytes = bi_v.to_bytes_le(); + bytes.resize(8, 0); + u64::from_le_bytes(bytes.try_into().unwrap()).into() + } + + pub(crate) fn to_le_bytes(self) -> [u8; 8] { + self.0.to_le_bytes() + } +} + +impl BitXor for Blake2bWord { + type Output = Self; + fn bitxor(self, rhs: Self) -> Self::Output { + Self(self.0 ^ rhs.0) + } +} + +impl Sub for Blake2bWord { + type Output = Self; + fn sub(self, rhs: Self) -> Self::Output { + Self(self.0 - rhs.0) + } +} + +impl From for Blake2bWord { + /// An u64 has a trivial conversion into a [Blake2bWord] + fn from(value: u64) -> Self { + Blake2bWord(value) + } +} + +impl From> for AssignedBlake2bWord { + fn from(value: AssignedCell) -> Self { + Self(value) + } +} + +/// Allows us to call the .assign_advice() method of the region with an Blake2bWord as its value +impl From<&Blake2bWord> for Rational { + fn from(value: &Blake2bWord) -> Self { + Self::Trivial(F::from(value.0)) + } +} + +/// This wrapper type on `AssignedCell` is designed to enforce type safety +/// on assigned Blake2bWords. It prevents the user from creating an [AssignedWord] +/// without using the designated entry points, which guarantee (with constraints) that the +/// assigned value is indeed in the range [0, 2^64 - 1]. +#[derive(Clone, Debug)] +pub(crate) struct AssignedBlake2bWord(AssignedCell); + +impl AssignedBlake2bWord { + /// Method that copies an [AssignedBlake2bWord] in the trace into another cell. + pub(crate) fn copy_advice_word( + &self, + region: &mut Region<'_, F>, + column: Column, + offset: usize, + annotation: &str, + ) -> Result { + let result = self.0.copy_advice(|| annotation, region, column, offset)?; + Ok(Self(result)) + } + + /// Method that assigns a fixed word in the trace. It's safe to use because it's a constant, + /// therefore it's constrained to a fixed value known by everyone. + pub(crate) fn assign_fixed_word( + region: &mut Region<'_, F>, + annotation: &str, + column: Column, + offset: usize, + word_value: Blake2bWord, + ) -> Result { + let result = + region.assign_advice_from_constant(|| annotation, column, offset, word_value)?; + Ok(Self(result)) + } + + /// Given a value that contains a field element, this method converts it into a Blake2bWord and + /// assigns the value into a cell. The word is range-checked both in circuit-building + /// time (synthesize) and constrained in the circuit. The idea is that only the base operations + /// can create an [AssignedBlake2bWord] from a Field value, since they're responsible to + /// activate the constraints over the cells in the trace. In this case, the [Decompose8] gate + /// is the responsible to create them. + pub(crate) fn assign_advice_word_from_field( + region: &mut Region<'_, F>, + annotation: &str, + column: Column, + offset: usize, + value: Value, + ) -> Result { + // Check value is in range + let word_value = value.map(|v| Blake2bWord::new_from_field(v)); + // Create AssignedCell with the same value but different type + Self::assign_advice_word(region, annotation, column, offset, word_value) + } + + /// Given a value that contains a Blake2bWord, this method assigns the value into a cell + pub(crate) fn assign_advice_word( + region: &mut Region<'_, F>, + annotation: &str, + column: Column, + offset: usize, + word_value: Value, + ) -> Result { + Ok(Self(region.assign_advice(|| annotation, column, offset, || word_value)?)) + } + + pub(crate) fn value(&self) -> Value { + self.0.value().cloned() + } + + pub(crate) fn cell(&self) -> Cell { + self.0.cell() + } +} diff --git a/third_party/blake2b_halo2/src/types/byte.rs b/third_party/blake2b_halo2/src/types/byte.rs new file mode 100644 index 000000000..1f754722a --- /dev/null +++ b/third_party/blake2b_halo2/src/types/byte.rs @@ -0,0 +1,117 @@ +use std::ops::BitXor; +use ff::PrimeField; +use midnight_proofs::circuit::{Cell, Region, Value}; +use midnight_proofs::plonk::{Advice, Column, Error}; +use midnight_proofs::utils::rational::Rational; +use super::*; + +/// The inner type of AssignedByte. A wrapper around `u8` +#[derive(Copy, Clone, Debug)] +pub struct Byte(pub u8); + +impl Byte { + /// Creates a new [Byte] element. When the byte is created, it is constrained to be in the + /// range [0, 255]. + pub(crate) fn new_from_field(field: F) -> Self { + let bi_v = get_word_biguint_from_le_field(field); + #[cfg(not(test))] + assert!(bi_v <= BigUint::from(255u8)); //[zhiyong]: no need to check in CPU, since it will be constrained in the circuit anyway + Byte(bi_v.to_bytes_le().first().copied().unwrap()) + } +} + +impl BitXor for Byte { + type Output = Self; + fn bitxor(self, rhs: Self) -> Self::Output { + Self(self.0 ^ rhs.0) + } +} + +/// Allows us to call the .assign_advice() method of the region with a Byte as its value +impl From<&Byte> for Rational { + fn from(value: &Byte) -> Self { + Self::Trivial(F::from(value.0 as u64)) + } +} + +/// This wrapper type on `AssignedCell` is designed to enforce type safety +/// on assigned bytes. It prevents the user from creating an `AssignedByte` +/// without using the designated entry points, which guarantee (with +/// constraints) that the assigned value is indeed in the range [0, 256). +#[derive(Clone, Debug)] +pub struct AssignedByte(AssignedCell); + +impl AssignedByte { + /// This method takes an [AssignedNative], copies it to another cell in the circuit as an + /// [AssignedByte]. The range-check is performed in synthesize time, but + /// WARNING: the caller of this method should allways constrain the value to be a byte in the + /// circuit. That's why only the base operations can create an [AssignedByte] from a Field value, + /// since they're responsible to activate the constraints over the cells in the trace. + pub(crate) fn copy_advice_byte_from_native( + region: &mut Region<'_, F>, + annotation: &str, + column: Column, + offset: usize, + cell_to_copy: AssignedNative, + ) -> Result { + // Check value is in range + let byte_value = cell_to_copy.value().map(|v| Byte::new_from_field(*v)); + // Create AssignedCell with the same value but different type + let assigned_byte = + Self(region.assign_advice(|| annotation, column, offset, || byte_value)?); + // Constrain cells have equal values + region.constrain_equal(cell_to_copy.cell(), assigned_byte.cell())?; + + Ok(assigned_byte) + } + + /// This method takes an [AssignedByte], and copies it to another cell in the circuit. + /// The range-check is not needed here, since we're copying a cell that should already have + /// been constrained. + pub(crate) fn copy_advice_byte( + region: &mut Region<'_, F>, + annotation: &str, + column: Column, + offset: usize, + cell_to_copy: AssignedByte, + ) -> Result { + // Check value is in range + let byte_value = cell_to_copy.0.value().map(|v| Byte(v.0)); + // Create AssignedCell with the same value but different type + let assigned_byte = + Self(region.assign_advice(|| annotation, column, offset, || byte_value)?); + // Constrain cells have equal values + region.constrain_equal(cell_to_copy.cell(), assigned_byte.cell())?; + + Ok(assigned_byte) + } + + /// Given a Byte value, it creates an [AssignedBlake2bWord] with its value. + /// WARNING: this method is only available to the base operations because they should make sure + /// that constrains over the byte values of these cells are enforced. + pub(crate) fn assign_advice_byte( + region: &mut Region<'_, F>, + annotation: &str, + column: Column, + offset: usize, + byte_value: Value, + ) -> Result, Error> { + Ok(Self(region.assign_advice(|| annotation, column, offset, || byte_value)?)) + } + + /// Gets the inner cell of an assigned byte. + pub fn cell(&self) -> Cell { + self.0.cell() + } + + /// Gets the inner value of an assigned byte. + pub fn value(&self) -> Value { + self.0.value().cloned() + } +} + +impl From> for AssignedCell { + fn from(value: AssignedByte) -> Self { + value.0 + } +} diff --git a/third_party/blake2b_halo2/src/types/mod.rs b/third_party/blake2b_halo2/src/types/mod.rs new file mode 100644 index 000000000..de81a10a0 --- /dev/null +++ b/third_party/blake2b_halo2/src/types/mod.rs @@ -0,0 +1,42 @@ +//! Basic types for the blake2b chip. + +/// This module holds types that exist across our code to explicitly state that a value is in a +/// given range. Everytime you see an AssignedBit, AssignedByte, AssignedBlake2bWord or AssignedRow, +/// you can be certain that all their values were range checked (both in the synthesize and in the +/// circuit constraints). +/// +/// All these types are created in a context where its value has been constrained by a circuit +/// restriction to be in range. +use ff::PrimeField; +use midnight_proofs::circuit::AssignedCell; +use num_bigint::BigUint; + +/// Native type for an [AssignedCell] that hasn't been constrained yet +pub type AssignedNative = AssignedCell; + +/// Module for assigned bits. +pub mod bit; +/// Module for assigned bytes. +pub mod byte; +/// Module for assigned blake2b words. +pub mod blake2b_word; +/// Module for assigned blake2b rows. +pub mod row; + +/// Given a field element and a limb index in little endian form, this function checks that the +/// field element is in range [0, 2^64-1]. If it's not, it will fail. +/// We assume that the internal representation of the field is in little endian form. If it's +/// not, the result is undefined and probably incorrect. +/// Finally, it returns a [BigUint] holding the field element value. +fn get_word_biguint_from_le_field(fe: F) -> BigUint { + let field_internal_representation = fe.to_repr(); // Should be in little-endian + let (bytes, zeros) = field_internal_representation.as_ref().split_at(8); + + let field_is_out_of_range = zeros.iter().any(|&el| el != 0u8); + + if field_is_out_of_range { + panic!("Arguments to the function are incorrect") + } else { + BigUint::from_bytes_le(bytes) + } +} diff --git a/third_party/blake2b_halo2/src/types/row.rs b/third_party/blake2b_halo2/src/types/row.rs new file mode 100644 index 000000000..6b1e676d0 --- /dev/null +++ b/third_party/blake2b_halo2/src/types/row.rs @@ -0,0 +1,21 @@ +use ff::PrimeField; +use crate::types::blake2b_word::AssignedBlake2bWord; +use crate::types::byte::AssignedByte; + +/// We use this type to model the Row we generally use along this circuit. This row has the +/// following shape: +/// full_number | limb_0 | limb_1 | limb_2 | limb_3 | limb_4 | limb_5 | limb_6 | limb_7 +/// +/// Where full_number is a Blake2bWord (64 bits) and the limbs constitute the little endian repr +/// of the full_number (each limb is an AssignedByte) +#[derive(Debug)] +pub(crate) struct AssignedRow { + pub(crate) full_number: AssignedBlake2bWord, + pub(crate) limbs: [AssignedByte; 8], +} + +impl AssignedRow { + pub(crate) fn new(full_number: AssignedBlake2bWord, limbs: [AssignedByte; 8]) -> Self { + Self { full_number, limbs } + } +} diff --git a/third_party/blake2b_halo2/src/usage_utils/blake2b_circuit.rs b/third_party/blake2b_halo2/src/usage_utils/blake2b_circuit.rs new file mode 100644 index 000000000..1c520fd39 --- /dev/null +++ b/third_party/blake2b_halo2/src/usage_utils/blake2b_circuit.rs @@ -0,0 +1,126 @@ +//! This is an example circuit of how you should use the Blake2b chip + +use crate::blake2b::blake2b_chip::{Blake2bChip, Blake2bConfig}; +use crate::types::AssignedNative; +use ff::PrimeField; +use midnight_proofs::circuit::{Layouter, SimpleFloorPlanner, Value}; +use midnight_proofs::plonk::{Advice, Circuit, Column, ConstraintSystem, Error, Instance}; +use std::array; + +/// The struct of the circuit. It contains the input and key that will be hashed. Also +/// the sizes of the input, key and output. +#[derive(Clone, Debug)] +pub struct Blake2bCircuit { + /// The input and the key should be unknown for the verifier. + input: Vec>, + key: Vec>, + /// All the sizes should be known at circuit building time, so we don't store them as values. + input_size: usize, + key_size: usize, + output_size: usize, +} + +impl Circuit for Blake2bCircuit { + type Config = (Blake2bConfig, Column); + type Params = (); + type FloorPlanner = SimpleFloorPlanner; + + fn without_witnesses(&self) -> Self { + let input_size = self.input_size; + let key_size = self.key_size; + let output_size = self.output_size; + Self { + input: vec![Value::unknown(); input_size], + input_size, + key: vec![Value::unknown(); key_size], + key_size, + output_size, + } + } + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + let full_number_u64 = meta.advice_column(); + let limbs: [Column; 8] = array::from_fn(|_| meta.advice_column()); + let constant_col = meta.fixed_column(); + let expected_final_state = meta.instance_column(); + meta.enable_equality(expected_final_state); + (Blake2bChip::configure(meta, constant_col, full_number_u64, limbs), expected_final_state) + } + + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + // The input bytes are assigned in the circuit before calling the hash function. + // They're not constrained to be in the range [0,255] here, but they are when used inside + // the blake2b chip. This means that the chip does not expect the inputs to be bytes, but + // the execution will fail if they're not. + let assigned_input = + Self::assign_inputs_to_the_trace(config.0.clone(), &mut layouter, &self.input)?; + let assigned_key = + Self::assign_inputs_to_the_trace(config.0.clone(), &mut layouter, &self.key)?; + + // Initialising the chip and calling the hash. + let chip = Blake2bChip::new(&config.0); + chip.load(&mut layouter)?; + let result = chip.hash(&mut layouter, &assigned_input, &assigned_key, self.output_size)?; + + // Assert results + for (i, global_state_byte_cell) in result.iter().enumerate().take(self.output_size) { + layouter.constrain_instance(global_state_byte_cell.cell(), config.1, i)?; + } + Ok(()) + } +} + +impl Blake2bCircuit { + /// This method creates a new instance of the circuit with the given input, key and output sizes. + pub fn new( + input: Vec>, + input_size: usize, + key: Vec>, + key_size: usize, + output_size: usize, + ) -> Self { + Self { + input, + input_size, + key, + key_size, + output_size, + } + } + + /// Here the inputs are stored in the trace. It doesn't really matter how they're stored, this + /// specific circuit uses the limb columns to do it but that's arbitrary. + fn assign_inputs_to_the_trace( + config: Blake2bConfig, + layouter: &mut impl Layouter, + input: &[Value], + ) -> Result>, Error> { + let result = layouter.assign_region( + || "Inputs", + |mut region| { + let inner_result = input + .iter() + .enumerate() + .map(|(index, input_byte)| { + let row = index / 8; + let column = index % 8; + region + .assign_advice( + || format!("Input column: {row}, row: {column}"), + config.limbs[column], + row, + || *input_byte, + ) + .unwrap() + }) + .collect::>(); + Ok(inner_result) + }, + )?; + Ok(result) + } +} diff --git a/third_party/blake2b_halo2/src/usage_utils/circuit_runner.rs b/third_party/blake2b_halo2/src/usage_utils/circuit_runner.rs new file mode 100644 index 000000000..a688224e0 --- /dev/null +++ b/third_party/blake2b_halo2/src/usage_utils/circuit_runner.rs @@ -0,0 +1,188 @@ +//! Circuit runner module for creating Blake2bCircuit, synthesizing, proving and verifying it. +//! It can work with both Mock Prover and Real Prover. + +use blake2b_simd::State as Blake2bState; +use midnight_proofs::dev::MockProver; +use midnight_curves::bls12_381::{Bls12, Fq}; +use midnight_proofs::{ + plonk::{create_proof, keygen_pk, keygen_vk_with_k, prepare, ProvingKey, VerifyingKey}, + poly::{ + commitment::Guard, + kzg::{params::ParamsKZG, KZGCommitmentScheme}, + }, + transcript::{CircuitTranscript, Transcript}, +}; +use midnight_proofs::circuit::Value; +use midnight_proofs::plonk::Error; +use crate::usage_utils::blake2b_circuit::Blake2bCircuit; + +/// The inputs for the Blake2bCircuit. This helps us to avoid passing multiple parameters to the +/// methods that create circuits +pub type Blake2bCircuitInputs = (Vec>, usize, Vec>, usize, [Fq; 64], usize); + +/// Circuit runner struct +#[derive(Debug)] +pub struct CircuitRunner; + +/// Circuit runner methods for Mock Prover +impl CircuitRunner { + /// Preprocess inputs, synthesize, prove and verify the circuit using Mock Prover + pub fn mocked_preprocess_inputs_synthesize_prove_and_verify( + input: &String, + key: &String, + expected: &String, + ) { + let circuit_inputs = Self::prepare_parameters_for_test(input, key, expected); + + let circuit = Self::create_circuit_for_packed_inputs(circuit_inputs.clone()); + let prover = Self::mock_prove_with_public_inputs_ref(&circuit_inputs.4, &circuit); + Self::verify_mock_prover(prover); + } + + /// Verify the circuit using Mock Prover + pub fn verify_mock_prover(prover: MockProver) { + prover.verify().unwrap() + } + + /// Create and run the Mock Prover using public inputs + pub fn mock_prove_with_public_inputs_ref( + expected_output_fields: &[Fq], + circuit: &Blake2bCircuit, + ) -> MockProver { + MockProver::run(17, circuit, vec![expected_output_fields.to_vec()]).unwrap() + } + + /// Create circuit for the given inputs + pub fn create_circuit_for_inputs( + input_values: Vec>, + input_size: usize, + key_values: Vec>, + key_size: usize, + output_size: usize, + ) -> Blake2bCircuit { + Blake2bCircuit::::new(input_values, input_size, key_values, key_size, output_size) + } + + /// Create circuit for the given inputs. In this function the inputs are packed in a + /// Blake2bCircuitInputs struct to avoid passing multiple parameters to the function + pub fn create_circuit_for_packed_inputs(ci: Blake2bCircuitInputs) -> Blake2bCircuit { + Blake2bCircuit::::new(ci.0, ci.1, ci.2, ci.3, ci.5) + } + + /// Convert the input, key and expected output in byte blocks + /// For the input and key, blocks are made of values, since they are private inputs of the + /// circuit + pub fn prepare_parameters_for_test( + input: &String, + key: &String, + expected: &String, + ) -> Blake2bCircuitInputs { + // INPUT + let input_size = input.len() / 2; // Amount of bytes + let input_bytes = hex::decode(input).expect("Invalid hex string"); + let input_values = + input_bytes.iter().map(|x| Value::known(Fq::from(*x as u64))).collect::>(); + + // OUTPUT + let (expected_output, output_size) = Self::formed_output_block_for(expected); + let expected_output_fields: [Fq; 64] = expected_output + .iter() + .map(|x| Fq::from(*x as u64)) + .collect::>() + .try_into() + .unwrap(); + + // KEY + let key_size = key.len() / 2; // Amount of bytes + let key_bytes = hex::decode(key).expect("Invalid hex string"); + let key_values = + key_bytes.iter().map(|x| Value::known(Fq::from(*x as u64))).collect::>(); + + (input_values, input_size, key_values, key_size, expected_output_fields, output_size) + } + + /// Convert the expected output of the circuit in byte blocks + pub fn formed_output_block_for(output: &String) -> ([u8; 64], usize) { + let output_block_size = output.len() / 2; // Amount of bytes + let output_bytes = hex::decode(output).expect("Invalid hex string"); + (output_bytes.try_into().unwrap(), output_block_size) + } +} + +/// Circuit runner methods for Real Prover +impl CircuitRunner { + /// Preprocess inputs, synthesize, prove and verify the circuit using a real prover + pub fn real_preprocess_inputs_synthesize_prove_and_verify( + input: String, + out: String, + key: String, + ) -> Result<(), Error> { + let circuit_inputs = Self::prepare_parameters_for_test(&input, &key, &out); + + let circuit: Blake2bCircuit = + Self::create_circuit_for_packed_inputs(circuit_inputs.clone()); + + let params = ParamsKZG::::unsafe_setup(17, &mut rand::thread_rng()); + let vk: VerifyingKey> = Self::create_vk(&circuit, ¶ms); + let pk: ProvingKey> = Self::create_pk(&circuit, vk); + let proof = Self::create_proof(&circuit_inputs.4, circuit, ¶ms, &pk); + Self::verify(&circuit_inputs.4, ¶ms, pk, &proof) + } + + /// Create the verifying key for the given circuit and parameters + pub fn create_vk( + circuit: &Blake2bCircuit, + params: &ParamsKZG, + ) -> VerifyingKey> { + keygen_vk_with_k(params, circuit, 17).expect("Verifying key should be created") + } + + /// Create the proving key for the given circuit and parameters + pub fn create_pk( + circuit: &Blake2bCircuit, + vk: VerifyingKey>, + ) -> ProvingKey> { + keygen_pk(vk.clone(), circuit).expect("Proving key should be created") + } + + /// Create the proof for the given circuit and parameters + pub fn create_proof( + expected_output_fields: &[Fq], + circuit: Blake2bCircuit, + params: &ParamsKZG, + pk: &ProvingKey>, + ) -> Vec { + let mut transcript = CircuitTranscript::::init(); + create_proof( + params, + pk, + &[circuit], + 0, + &[&[expected_output_fields]], + rand::thread_rng(), + &mut transcript, + ) + .expect("Proof generation should work"); + transcript.finalize() + } + + /// Verify the proof for the given circuit and parameters + pub fn verify( + expected_output_fields: &[Fq], + params: &ParamsKZG, + pk: ProvingKey>, + proof: &[u8], + ) -> Result<(), Error> { + let mut transcript = CircuitTranscript::::init_from_bytes(proof); + + assert!(prepare::, _>( + pk.get_vk(), + &[&[]], + &[&[expected_output_fields]], + &mut transcript, + )? + .verify(¶ms.verifier_params()) + .is_ok()); + Ok(()) + } +} diff --git a/third_party/blake2b_halo2/src/usage_utils/mod.rs b/third_party/blake2b_halo2/src/usage_utils/mod.rs new file mode 100644 index 000000000..5a3d16b41 --- /dev/null +++ b/third_party/blake2b_halo2/src/usage_utils/mod.rs @@ -0,0 +1,6 @@ +//! Module that implements an example Blake2bCircuit that uses our Blake2bChip +//! It also has a CircuitRunner that helps to preprocess inputs, synthesize, prove and verify +//! the circuit. Used for testing and benchmarking purposes. + +pub mod blake2b_circuit; +pub mod circuit_runner; diff --git a/third_party/blake2b_halo2/test_vector.json b/third_party/blake2b_halo2/test_vector.json new file mode 100644 index 000000000..ccd933c27 --- /dev/null +++ b/third_party/blake2b_halo2/test_vector.json @@ -0,0 +1,3074 @@ +[ + { + "hash": "blake2b", + "in": "", + "key": "", + "out": "786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce" + }, + { + "hash": "blake2b", + "in": "00", + "key": "", + "out": "2fa3f686df876995167e7c2e5d74c4c7b6e48f8068fe0e44208344d480f7904c36963e44115fe3eb2a3ac8694c28bcb4f5a0f3276f2e79487d8219057a506e4b" + }, + { + "hash": "blake2b", + "in": "0001", + "key": "", + "out": "1c08798dc641aba9dee435e22519a4729a09b2bfe0ff00ef2dcd8ed6f8a07d15eaf4aee52bbf18ab5608a6190f70b90486c8a7d4873710b1115d3debbb4327b5" + }, + { + "hash": "blake2b", + "in": "000102", + "key": "", + "out": "40a374727302d9a4769c17b5f409ff32f58aa24ff122d7603e4fda1509e919d4107a52c57570a6d94e50967aea573b11f86f473f537565c66f7039830a85d186" + }, + { + "hash": "blake2b", + "in": "00010203", + "key": "", + "out": "77ddf4b14425eb3d053c1e84e3469d92c4cd910ed20f92035e0c99d8a7a86cecaf69f9663c20a7aa230bc82f60d22fb4a00b09d3eb8fc65ef547fe63c8d3ddce" + }, + { + "hash": "blake2b", + "in": "0001020304", + "key": "", + "out": "cbaa0ba7d482b1f301109ae41051991a3289bc1198005af226c5e4f103b66579f461361044c8ba3439ff12c515fb29c52161b7eb9c2837b76a5dc33f7cb2e2e8" + }, + { + "hash": "blake2b", + "in": "000102030405", + "key": "", + "out": "f95d45cf69af5c2023bdb505821e62e85d7caedf7beda12c0248775b0c88205eeb35af3a90816f6608ce7dd44ec28db1140614e1ddebf3aa9cd1843e0fad2c36" + }, + { + "hash": "blake2b", + "in": "00010203040506", + "key": "", + "out": "8f945ba700f2530e5c2a7df7d5dce0f83f9efc78c073fe71ae1f88204a4fd1cf70a073f5d1f942ed623aa16e90a871246c90c45b621b3401a5ddbd9df6264165" + }, + { + "hash": "blake2b", + "in": "0001020304050607", + "key": "", + "out": "e998e0dc03ec30eb99bb6bfaaf6618acc620320d7220b3af2b23d112d8e9cb1262f3c0d60d183b1ee7f096d12dae42c958418600214d04f5ed6f5e718be35566" + }, + { + "hash": "blake2b", + "in": "000102030405060708", + "key": "", + "out": "6a9a090c61b3410aede7ec9138146ceb2c69662f460c3da53c6515c1eb31f41ca3d280e567882f95cf664a94147d78f42cfc714a40d22ef19470e053493508a2" + }, + { + "hash": "blake2b", + "in": "00010203040506070809", + "key": "", + "out": "29102511d749db3cc9b4e335fa1f5e8faca8421d558f6a3f3321d50d044a248ba595cfc3efd3d2adc97334da732413f5cbf4751c362ba1d53862ac1e8dabeee8" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a", + "key": "", + "out": "c97a4779d47e6f77729b5917d0138abb35980ab641bd73a8859eb1ac98c05362ed7d608f2e9587d6ba9e271d343125d40d933a8ed04ec1fe75ec407c7a53c34e" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b", + "key": "", + "out": "10f0dc91b9f845fb95fad6860e6ce1adfa002c7fc327116d44d047cd7d5870d772bb12b5fac00e02b08ac2a0174d0446c36ab35f14ca31894cd61c78c849b48a" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c", + "key": "", + "out": "dea9101cac62b8f6a3c650f90eea5bfae2653a4eafd63a6d1f0f132db9e4f2b1b662432ec85b17bcac41e775637881f6aab38dd66dcbd080f0990a7a6e9854fe" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d", + "key": "", + "out": "441ffaa08cd79dff4afc9b9e5b5620eec086730c25f661b1d6fbfbd1cec3148dd72258c65641f2fca5eb155fadbcabb13c6e21dc11faf72c2a281b7d56145f19" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e", + "key": "", + "out": "444b240fe3ed86d0e2ef4ce7d851edde22155582aa0914797b726cd058b6f45932e0e129516876527b1dd88fc66d7119f4ab3bed93a61a0e2d2d2aeac336d958" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f", + "key": "", + "out": "bfbabbef45554ccfa0dc83752a19cc35d5920956b301d558d772282bc867009168e9e98606bb5ba73a385de5749228c925a85019b71f72fe29b3cd37ca52efe6" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f10", + "key": "", + "out": "9c4d0c3e1cdbbf485bec86f41cec7c98373f0e09f392849aaa229ebfbf397b22085529cb7ef39f9c7c2222a514182b1effaa178cc3687b1b2b6cbcb6fdeb96f8" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f1011", + "key": "", + "out": "477176b3bfcbadd7657c23c24625e4d0d674d1868f006006398af97aa41877c8e70d3d14c3bbc9bbcdcea801bd0e1599af1f3eec67405170f4e26c964a57a8b7" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112", + "key": "", + "out": "a78c490eda3173bb3f10dee52f110fb1c08e0302230b85ddd7c11257d92de148785ef00c039c0bb8eb9808a35b2d8c080f572859714c9d4069c5bcaf090e898e" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f10111213", + "key": "", + "out": "58d023397beb5b4145cb2255b07d74290b36d9fd1e594afbd8eea47c205b2efbfe6f46190faf95af504ab072e36f6c85d767a321bfd7f22687a4abbf494a689c" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f1011121314", + "key": "", + "out": "4001ec74d5a46fd29c2c3cdbe5d1b9f20e51a941be98d2a4e1e2fbf866a672121db6f81a514cfd10e7358d571bdba48e4ce708b9d124894bc0b5ed554935f73a" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415", + "key": "", + "out": "ccd1b22dab6511225d2401ea2d8625d206a12473cc732b615e5640cefff0a4adf971b0e827a619e0a80f5db9ccd0962329010d07e34a2064e731c520817b2183" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f10111213141516", + "key": "", + "out": "b4a0a9e3574edb9e1e72aa31e39cc5f30dbf943f8cabc408449654a39131e66d718a18819143e3ea96b4a1895988a1c0056cf2b6e04f9ac19d657383c2910c44" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f1011121314151617", + "key": "", + "out": "447becab16630608d39f4f058b16f7af95b85a76aa0fa7cea2b80755fb76e9c804f2ca78f02643c915fbf2fce5e19de86000de03b18861815a83126071f8a37b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718", + "key": "", + "out": "54e6dab9977380a5665822db93374eda528d9beb626f9b94027071cb26675e112b4a7fec941ee60a81e4d2ea3ff7bc52cfc45dfbfe735a1c646b2cf6d6a49b62" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f10111213141516171819", + "key": "", + "out": "3ea62625949e3646704d7e3c906f82f6c028f540f5f72a794b0c57bf97b7649bfeb90b01d3ca3e829de21b3826e6f87014d3c77350cb5a15ff5d468a81bec160" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a", + "key": "", + "out": "213cfe145c54a33691569980e5938c8883a46d84d149c8ff1a67cd287b4d49c6da69d3a035443db085983d0efe63706bd5b6f15a7da459e8d50a19093db55e80" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b", + "key": "", + "out": "5716c4a38f38db104e494a0a27cbe89a26a6bb6f499ec01c8c01aa7cb88497e75148cd6eee12a7168b6f78ab74e4be749251a1a74c38c86d6129177e2889e0b6" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c", + "key": "", + "out": "030460a98bdf9ff17cd96404f28fc304f2b7c04eaade53677fd28f788ca22186b8bc80dd21d17f8549c711aff0e514e19d4e15f5990252a03e082f28dc2052f6" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d", + "key": "", + "out": "19e7f1ccee88a10672333e390cf22013a8c734c6cb9eab41f17c3c8032a2e4aca0569ea36f0860c7a1af28fa476840d66011168859334a9e4ef9cc2e61a0e29e" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e", + "key": "", + "out": "29f8b8c78c80f2fcb4bdf7825ed90a70d625ff785d262677e250c04f3720c888d03f8045e4edf3f5285bd39d928a10a7d0a5df00b8484ac2868142a1e8bea351" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "key": "", + "out": "5c52920a7263e39d57920ca0cb752ac6d79a04fef8a7a216a1ecb7115ce06d89fd7d735bd6f4272555dba22c2d1c96e6352322c62c5630fde0f4777a76c3de2c" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + "key": "", + "out": "83b098f262251bf660064a9d3511ce7687a09e6dfbb878299c30e93dfb43a9314db9a600337db26ebeedaf2256a96dabe9b29e7573ad11c3523d874dde5be7ed" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021", + "key": "", + "out": "9447d98aa5c9331352f43d3e56d0a9a9f9581865998e2885cc56dd0a0bd5a7b50595bd10f7529bcd31f37dc16a1465d594079667da2a3fcb70401498837cedeb" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122", + "key": "", + "out": "867732f2feeb23893097561ac710a4bff453be9cfbedba8ba324f9d312a82d732e1b83b829fdcd177b882ca0c1bf544b223be529924a246a63cf059bfdc50a1b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223", + "key": "", + "out": "f15ab26d4cdfcf56e196bb6ba170a8fccc414de9285afd98a3d3cf2fb88fcbc0f19832ac433a5b2cc2392a4ce34332987d8d2c2bef6c3466138db0c6e42fa47b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021222324", + "key": "", + "out": "2813516d68ed4a08b39d648aa6aacd81e9d655ecd5f0c13556c60fdf0d333ea38464b36c02baccd746e9575e96c63014f074ae34a0a25b320f0fbedd6acf7665" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425", + "key": "", + "out": "d3259afca8a48962fa892e145acf547f26923ae8d4924c8a531581526b04b44c7af83c643ef5a0bc282d36f3fb04c84e28b351f40c74b69dc7840bc717b6f15f" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223242526", + "key": "", + "out": "f14b061ae359fa31b989e30332bfe8de8cc8cdb568e14be214a2223b84caab7419549ecfcc96ce2acec119485d87d157d3a8734fc426597d64f36570ceaf224d" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021222324252627", + "key": "", + "out": "55e70b01d1fbf8b23b57fb62e26c2ce54f13f8fa2464e6eb98d16a6117026d8b90819012496d4071ebe2e59557ece3519a7aa45802f9615374877332b73490b3" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728", + "key": "", + "out": "25261eb296971d6e4a71b2928e64839c67d422872bf9f3c31993615222de9f8f0b2c4be8548559b4b354e736416e3218d4e8a1e219a4a6d43e1a9a521d0e75fc" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223242526272829", + "key": "", + "out": "08307f347c41294e34bb54cb42b1522d22f824f7b6e5db50fda096798e181a8f026fa27b4ae45d52a62caf9d5198e24a4913c6671775b2d723c1239bfbf016d7" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a", + "key": "", + "out": "1e5c62e7e9bfa1b118747a2de08b3ca10112af96a46e4b22c3fc06f9bfee4eb5c49e057a4a4886234324572576bb9b5ecfde0d99b0de4f98ec16e4d1b85fa947" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b", + "key": "", + "out": "c74a77395fb8bc126447454838e561e962853dc7eb49a1e3cb67c3d0851f3e39517be8c350ac910903d49cd2bfdf545c99316d0346170b739f0add5d533c2cfc" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c", + "key": "", + "out": "0dd57b423cc01eb2861391eb886a0d17079b933fc76eb3fc08a19f8a74952cb68f6bcdc644f77370966e4d13e80560bcf082ef0479d48fbbab4df03b53a4e178" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d", + "key": "", + "out": "4d8dc3923edccdfce70072398b8a3da5c31fcb3ee3b645c85f717cbaeb4b673a19394425a585bfb464d92f1597d0b754d163f97ced343b25db5a70ef48ebb34f" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e", + "key": "", + "out": "f0a50553e4dfb0c4e3e3d3ba82034857e3b1e50918f5b8a7d698e10d242b0fb544af6c92d0c3aaf9932220416117b4e78ecb8a8f430e13b82a5915290a5819c5" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f", + "key": "", + "out": "b15543f3f736086627cc5365e7e8988c2ef155c0fd4f428961b00d1526f04d6d6a658b4b8ed32c5d8621e7f4f8e8a933d9ecc9dd1b8333cbe28cfc37d9719e1c" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30", + "key": "", + "out": "7b4fa158e415fef023247264cbbe15d16d91a44424a8db707eb1e2033c30e9e1e7c8c0864595d2cb8c580eb47e9d16abbd7e44e824f7cedb7def57130e52cfe9" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031", + "key": "", + "out": "60424ff23234c34dc9687ad502869372cc31a59380186bc2361c835d972f49666eb1ac69629de646f03f9b4db9e2ace093fbfdf8f20ab5f98541978be8ef549f" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132", + "key": "", + "out": "7406018ce704d84f5eb9c79fea97da345699468a350ee0b2d0f3a4bf2070304ea862d72a51c57d3064947286f531e0eaf7563702262e6c724abf5ed8c8398d17" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233", + "key": "", + "out": "14ef5c6d647b3bd1e6e32006c231199810de5c4dc88e70240273b0ea18e651a3eb4f5ca3114b8a56716969c7cda27e0c8db832ad5e89a2dc6cb0adbe7d93abd1" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334", + "key": "", + "out": "38cf6c24e3e08bcf1f6cf3d1b1f65b905239a3118033249e448113ec632ea6dc346feeb2571c38bd9a7398b2221280328002b23e1a45adaffe66d93f6564eaa2" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435", + "key": "", + "out": "6cd7208a4bc7e7e56201bbba02a0f489cd384abe40afd4222f158b3d986ee72a54c50fb64fd4ed2530eda2c8af2928a0da6d4f830ae1c9db469dfd970f12a56f" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233343536", + "key": "", + "out": "659858f0b5c9edab5b94fd732f6e6b17c51cc096104f09beb3afc3aa467c2ecf885c4c6541effa9023d3b5738ae5a14d867e15db06fe1f9d1127b77e1aabb516" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637", + "key": "", + "out": "26cca0126f5d1a813c62e5c71001c046f9c92095704550be5873a495a999ad010a4f79491f24f286500adce1a137bc2084e4949f5b7294cefe51ecaff8e95cba" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738", + "key": "", + "out": "4147c1f55172788c5567c561feef876f621fff1ce87786b8467637e70dfbcd0dbdb6415cb600954ab9c04c0e457e625b407222c0fe1ae21b2143688ada94dc58" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233343536373839", + "key": "", + "out": "5b1bf154c62a8af6e93d35f18f7f90abb16a6ef0e8d1aecd118bf70167bab2af08935c6fdc0663ce74482d17a8e54b546d1c296631c65f3b522a515839d43d71" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a", + "key": "", + "out": "9f600419a4e8f4fb834c24b0f7fc13bf4e279d98e8a3c765ee934917403e3a66097182ea21453cb63ebbe8b73a9c2167596446438c57627f330badd4f569f7d6" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b", + "key": "", + "out": "457ef6466a8924fd8011a34471a5a1ac8ccd9bd0d07a97414ac943021ce4b9e4b9c8db0a28f016ed43b1542481990022147b313e194671131e708dd43a3ed7dc" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c", + "key": "", + "out": "9997b2194d9af6dfcb9143f41c0ed83d3a3f4388361103d38c2a49b280a581212715fd908d41c651f5c715ca38c0ce2830a37e00e508ced1bcdc320e5e4d1e2e" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d", + "key": "", + "out": "5c6bbf16baa180f986bd40a1287ed4c549770e7284858fc47bc21ab95ebbf3374b4ee3fd9f2af60f3395221b2acc76f2d34c132954049f8a3a996f1e32ec84e5" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e", + "key": "", + "out": "d10bf9a15b1c9fc8d41f89bb140bf0be08d2f3666176d13baac4d381358ad074c9d4748c300520eb026daeaea7c5b158892fde4e8ec17dc998dcd507df26eb63" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "key": "", + "out": "2fc6e69fa26a89a5ed269092cb9b2a449a4409a7a44011eecad13d7c4b0456602d402fa5844f1a7a758136ce3d5d8d0e8b86921ffff4f692dd95bdc8e5ff0052" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40", + "key": "", + "out": "fcbe8be7dcb49a32dbdf239459e26308b84dff1ea480df8d104eeff34b46fae98627b450c2267d48c0946a697c5b59531452ac0484f1c84e3a33d0c339bb2e28" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f4041", + "key": "", + "out": "a19093a6e3bcf5952f850f2030f69b9606f147f90b8baee3362da71d9f35b44ef9d8f0a7712ba1877fddcd2d8ea8f1e5a773d0b745d4725605983a2de901f803" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142", + "key": "", + "out": "3c2006423f73e268fa59d2920377eb29a4f9a8b462be15983ee3b85ae8a78e992633581a9099893b63db30241c34f643027dc878279af5850d7e2d4a2653073a" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40414243", + "key": "", + "out": "d0f2f2e3787653f77cce2fa24835785bbd0c433fc779465a115149905a9dd1cb827a628506d457fcf124a0c2aef9ce2d2a0a0f63545570d8667ff9e2eba07334" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f4041424344", + "key": "", + "out": "78a9fc048e25c6dcb5de45667de8ffdd3a93711141d594e9fa62a959475da6075ea8f0916e84e45ad911b75467077ee52d2c9aebf4d58f20ce4a3a00458b05d4" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445", + "key": "", + "out": "45813f441769ab6ed37d349ff6e72267d76ae6bb3e3c612ec05c6e02a12af5a37c918b52bf74267c3f6a3f183a8064ff84c07b193d08066789a01accdb6f9340" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40414243444546", + "key": "", + "out": "956da1c68d83a7b881e01b9a966c3c0bf27f68606a8b71d457bd016d4c41dd8a380c709a296cb4c6544792920fd788835771a07d4a16fb52ed48050331dc4c8b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f4041424344454647", + "key": "", + "out": "df186c2dc09caa48e14e942f75de5ac1b7a21e4f9f072a5b371e09e07345b0740c76177b01278808fec025eded9822c122afd1c63e6f0ce2e32631041063145c" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748", + "key": "", + "out": "87475640966a9fdcd6d3a3b5a2cca5c08f0d882b10243c0ec1bf3c6b1c37f2cd3212f19a057864477d5eaf8faed73f2937c768a0af415e84bbce6bd7de23b660" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40414243444546474849", + "key": "", + "out": "c3b573bbe10949a0fbd4ff884c446f2229b76902f9dfdbb8a0353da5c83ca14e8151bbaac82fd1576a009adc6f1935cf26edd4f1fb8da483e6c5cd9d8923adc3" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a", + "key": "", + "out": "b09d8d0bba8a7286e43568f7907550e42036d674e3c8fc34d8ca46f771d6466b70fb605875f6a863c877d12f07063fdc2e90ccd459b1910dcd52d8f10b2b0a15" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b", + "key": "", + "out": "af3a22bf75b21abfb0acd54422ba1b7300a952eff02ebeb65b5c234471a98df32f4f9643ce1904108a168767924280bd76c83f8c82d9a79d9259b195362a2a04" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c", + "key": "", + "out": "bf4ff2221b7e6957a724cd964aa3d5d0d9941f540413752f4699d8101b3e537508bf09f8508b317736ffd265f2847aa7d84bd2d97569c49d632aed9945e5fa5e" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d", + "key": "", + "out": "9c6b6b78199b1bdacb4300e31479fa622a6b5bc80d4678a6078f88a8268cd7206a2799e8d4621a464ef6b43dd8adffe97caf221b22b6b8778b149a822aefbb09" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e", + "key": "", + "out": "890656f09c99d280b5ecb381f56427b813751bc652c7828078b23a4af83b4e3a61fdbac61f89bee84ea6bee760c047f25c6b0a201c69a38fd6fd971af18588bb" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f", + "key": "", + "out": "31a046f7882ffe6f83ce472e9a0701832ec7b3f76fbcfd1df60fe3ea48fde1651254247c3fd95e100f9172731e17fd5297c11f4bb328363ca361624a81af797c" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f50", + "key": "", + "out": "27a60b2d00e7a671d47d0aec2a686a0ac04b52f40ab6629028eb7d13f4baa99ac0fe46ee6c814944f2f4b4d20e9378e4847ea44c13178091e277b87ea7a55711" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f5051", + "key": "", + "out": "8b5ccef194162c1f19d68f91e0b0928f289ec5283720840c2f73d253111238dcfe94af2b59c2c1ca2591901a7bc060e7459b6c47df0f71701a35cc0aa831b5b6" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152", + "key": "", + "out": "57ab6c4b2229aeb3b70476d803cd63812f107ce6da17fed9b17875e8f86c724f49e024cbf3a1b8b119c50357652b81879d2ade2d588b9e4f7cedba0e4644c9ee" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f50515253", + "key": "", + "out": "0190a8dac320a739f322e15731aa140ddaf5bed294d5c82e54fef29f214e18aafaa84f8be99af62950266b8f901f15dd4c5d35516fc35b4cab2e96e4695bbe1c" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f5051525354", + "key": "", + "out": "d14d7c4c415eeb0e10b159224bea127ebd84f9591c702a330f5bb7bb7aa44ea39de6ed01f18da7adf40cfb97c5d152c27528824b21e239526af8f36b214e0cfb" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455", + "key": "", + "out": "be28c4be706970488fac7d29c3bd5c4e986085c4c3332f1f3fd30973db614164ba2f31a78875ffdc150325c88327a9443ed04fdfe5be93876d1628560c764a80" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f50515253545556", + "key": "", + "out": "031da1069e3a2e9c3382e436ffd79df74b1ca6a8adb2deabe676ab45994cbc054f037d2f0eace858d32c14e2d1c8b46077308e3bdc2c1b53172ecf7a8c14e349" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f5051525354555657", + "key": "", + "out": "4665cef8ba4db4d0acb118f2987f0bb09f8f86aa445aa3d5fc9a8b346864787489e8fcecc125d17e9b56e12988eac5ecc7286883db0661b8ff05da2afff30fe4" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758", + "key": "", + "out": "63b7032e5f930cc9939517f9e986816cfbec2be59b9568b13f2ead05bae7777cab620c6659404f7409e4199a3be5f7865aa7cbdf8c4253f7e8219b1bd5f46fea" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f50515253545556575859", + "key": "", + "out": "9f09bf093a2b0ff8c2634b49e37f1b2135b447aa9144c9787dbfd92129316c99e88aab8a21fdef2372d1189aec500f95775f1f92bfb45545e4259fb9b7b02d14" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a", + "key": "", + "out": "f9f8493c68088807df7f6a2693d64ea59f03e9e05a223e68524ca32195a4734b654fcea4d2734c866cf95c889fb10c49159be2f5043dc98bb55e02ef7bdcb082" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b", + "key": "", + "out": "3c9a7359ab4febce07b20ac447b06a240b7fe1dae5439c49b60b5819f7812e4c172406c1aac316713cf0dded1038077258e2eff5b33913d9d95caeb4e6c6b970" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c", + "key": "", + "out": "ad6aab8084510e822cfce8625d62cf4de655f4763884c71e80bab9ac9d5318dba4a6033ed29084e65216c031606ca17615dcfe3ba11d26851ae0999ca6e232cf" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d", + "key": "", + "out": "156e9e6261374c9dc884f36e70f0fe1ab9297997b836fa7d170a9c9ebf575b881e7bcea44d6c0248d35597907154828955be19135852f9228815eca024a8adfb" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e", + "key": "", + "out": "4215407633f4cca9b6788be93e6aa3d963c7d6ce4b147247099f46a3acb500a30038cb3e788c3d29f132ad844e80e9e99251f6db96acd8a091cfc770af53847b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f", + "key": "", + "out": "1c077e279de6548523502b6df800ffdab5e2c3e9442eb838f58c295f3b147cef9d701c41c321283f00c71affa0619310399126295b78dd4d1a74572ef9ed5135" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f60", + "key": "", + "out": "f07a555f49fe481cf4cd0a87b71b82e4a95064d06677fdd90a0eb598877ba1c83d4677b393c3a3b6661c421f5b12cb99d20376ba7275c2f3a8f5a9b7821720da" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f6061", + "key": "", + "out": "b5911b380d20c7b04323e4026b38e200f534259233b581e02c1e3e2d8438d6c66d5a4eb201d5a8b75072c4ec29106334da70bc79521b0ced2cfd533f5ff84f95" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162", + "key": "", + "out": "01f070a09bae911296361f91aa0e8e0d09a7725478536d9d48c5fe1e5e7c3c5b9b9d6eb07796f6da57ae562a7d70e882e37adfde83f0c433c2cd363536bb22c8" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f60616263", + "key": "", + "out": "6f793eb4374a48b0775acaf9adcf8e45e54270c9475f004ad8d5973e2aca52747ff4ed04ae967275b9f9eb0e1ff75fb4f794fa8be9add7a41304868d103fab10" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f6061626364", + "key": "", + "out": "965f20f139765fcc4ce4ba3794675863cac24db472cd2b799d035bce3dbea502da7b524865f6b811d8c5828d3a889646fe64a380da1aa7c7044e9f245dced128" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465", + "key": "", + "out": "ec295b5783601244c30e4641e3b45be222c4dce77a58700f53bc8ec52a941690b4d0b087fb6fcb3f39832b9de8f75ec20bd43079811749cdc907edb94157d180" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f60616263646566", + "key": "", + "out": "61c72f8ccc91dbb54ca6750bc489672de09faedb8fdd4f94ff2320909a303f5d5a98481c0bc1a625419fb4debfbf7f8a53bb07ec3d985e8ea11e72d559940780" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f6061626364656667", + "key": "", + "out": "afd8145b259eefc8d12620c3c5b03e1ed8fd2ccefe0365078c80fd42c1770e28b44948f27e65a1886690110db814397b68e43d80d1ba16dfa358e739c898cfa3" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768", + "key": "", + "out": "552fc7893cf1ce933ada35c0da98844e41545e244c3157a1428d7b4c21f9cd7e4071aed77b7ca9f1c38fba32237412ef21a342742ec8324378f21e507fafdd88" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f60616263646566676869", + "key": "", + "out": "467a33fbadf5ebc52596ef86aaaefc6faba8ee651b1ce04de368a03a5a9040ef2835e00adb09abb3fbd2bce818a2413d0b0253b5bda4fc5b2f6f85f3fd5b55f2" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a", + "key": "", + "out": "22eff8e6dd5236f5f57d94ede874d6c9428e8f5d566f17cd6d1848cd752fe13c655cb10fbaaff76872f2bf2da99e15dc624075e1ec2f58a3f64072121838569e" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b", + "key": "", + "out": "9cec6bbf62c4bce4138abae1cbec8dad31950444e90321b1347196834c114b864af3f3cc3508f83751ffb4eda7c84d140734bb4263c3625c00f04f4c8068981b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c", + "key": "", + "out": "a8b60fa4fc2442f6f1514ad7402626920cc7c2c9f72124b8cba8ee2cb7c4586f658a4410cffcc0ab88343955e094c6af0d20d0c714fb0a988f543f300f58d389" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d", + "key": "", + "out": "8271cc45dfa5e4170e847e8630b952cf9c2aa777d06f26a7585b8381f188dacc7337391cfcc94b053dc4ec29cc17f077870428f1ac23fddda165ef5a3f155f39" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e", + "key": "", + "out": "bf23c0c25c8060e4f6995f1623a3bebecaa96e308680000a8aa3cd56bb1a6da099e10d9231b37f4519b2efd2c24de72f31a5f19535241b4a59fa3c03ceb790e7" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f", + "key": "", + "out": "877fd652c05281009c0a5250e7a3a671f8b18c108817fe4a874de22da8e45db11958a600c5f62e67d36cbf84474cf244a9c2b03a9fb9dc711cd1a2cab6f3fae0" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f70", + "key": "", + "out": "29df4d87ea444baf5bcdf5f4e41579e28a67de84149f06c03f110ea84f572a9f676addd04c4878f49c5c00accda441b1a387caceb2e993bb7a10cd8c2d6717e1" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f7071", + "key": "", + "out": "710dacb166844639cd7b637c274209424e2449dc35d790bbfa4f76177054a36b3b76fac0ca6e61df1e687000678ac0746df75d0a3954897681fd393a155a1bb4" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172", + "key": "", + "out": "c1d5f93b8dea1f2571babccbc01764541a0cda87e444d673c50966ca559c33354b3acb26e5d5781ffb28847a4b4754d77008c62a835835f500dea7c3b58bdae2" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f70717273", + "key": "", + "out": "a41e41271cdab8af4d72b104bfb2ad041ac4df14677da671d85640c4b187f50c2b66513c4619fbd5d5dc4fe65dd37b9042e9848dda556a504caa2b1c6afe4730" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f7071727374", + "key": "", + "out": "e7bcbacdc379c43d81ebadcb37781552fc1d753e8cf310d968392d06c91f1d64cc9e90ce1d22c32d277fc6cda433a4d442c762e9eacf2c259f32d64cf9da3a22" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475", + "key": "", + "out": "51755b4ac5456b13218a19c5b9242f57c4a981e4d4ecdce09a3193362b808a579345d4881c2607a56534dd7f21956aff72c2f4173a6e7b6cc2212ba0e3daee1f" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f70717273747576", + "key": "", + "out": "dcc2c4beb9c1f2607b786c20c631972347034c1cc02fcc7d02ff01099cfe1c6989840ac213923629113aa8bad713ccf0fe4ce13264fb32b8b0fe372da382544a" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f7071727374757677", + "key": "", + "out": "3d55176acea4a7e3a65ffa9fb10a7a1767199cf077cee9f71532d67cd7c73c9f93cfc37ccdcc1fdef50aad46a504a650d298d597a3a9fa95c6c40cb71fa5e725" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778", + "key": "", + "out": "d07713c005de96dd21d2eb8bbeca66746ea51a31ae922a3e74864889540a48db27d7e4c90311638b224bf0201b501891754848113c266108d0adb13db71909c7" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f70717273747576777879", + "key": "", + "out": "58983c21433d950caa23e4bc18543b8e601c204318532152daf5e159a0cd1480183d29285c05f129cb0cc3164687928086ffe380158df1d394c6ac0d4288bca8" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a", + "key": "", + "out": "8100a8dc528d2b682ab4250801ba33f02a3e94c54dac0ae1482aa21f51ef3a82f3807e6facb0aeb05947bf7aa2adcb034356f90fa4560ede02201a37e411ec1a" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b", + "key": "", + "out": "07025f1bb6c784f3fe49de5c14b936a5acacacaab33f6ac4d0e00ab6a12483d6bec00b4fe67c7ca5cc508c2a53efb5bfa5398769d843ff0d9e8b14d36a01a77f" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c", + "key": "", + "out": "ba6aefd972b6186e027a76273a4a723321a3f580cfa894da5a9ce8e721c828552c64dacee3a7fd2d743b5c35ad0c8efa71f8ce99bf96334710e2c2346e8f3c52" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d", + "key": "", + "out": "e0721e02517aedfa4e7e9ba503e025fd46e714566dc889a84cbfe56a55dfbe2fc4938ac4120588335deac8ef3fa229adc9647f54ad2e3472234f9b34efc46543" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e", + "key": "", + "out": "b6292669ccd38d5f01caae96ba272c76a879a45743afa0725d83b9ebb26665b731f1848c52f11972b6644f554c064fa90780dbbbf3a89d4fc31f67df3e5857ef" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f", + "key": "", + "out": "2319e3789c47e2daa5fe807f61bec2a1a6537fa03f19ff32e87eecbfd64b7e0e8ccff439ac333b040f19b0c4ddd11a61e24ac1fe0f10a039806c5dcc0da3d115" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f80", + "key": "", + "out": "f59711d44a031d5f97a9413c065d1e614c417ede998590325f49bad2fd444d3e4418be19aec4e11449ac1a57207898bc57d76a1bcf3566292c20c683a5c4648f" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f8081", + "key": "", + "out": "df0a9d0c212843a6a934e3902b2dd30d17fba5f969d2030b12a546d8a6a45e80cf5635f071f0452e9c919275da99bed51eb1173c1af0518726b75b0ec3bae2b5" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182", + "key": "", + "out": "a3eb6e6c7bf2fb8b28bfe8b15e15bb500f781ecc86f778c3a4e655fc5869bf2846a245d4e33b7b14436a17e63be79b36655c226a50ffbc7124207b0202342db5" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f80818283", + "key": "", + "out": "56d4cbcd070563426a017069425c2cd2ae540668287a5fb9dac432eb8ab1a353a30f2fe1f40d83333afe696a267795408a92fe7da07a0c1814cf77f36e105ee8" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f8081828384", + "key": "", + "out": "e59b9987d428b3eda37d80abdb16cd2b0aef674c2b1dda4432ea91ee6c935c684b48b4428a8cc740e579a30deff35a803013820dd23f14ae1d8413b5c8672aec" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485", + "key": "", + "out": "cd9fcc99f99d4cc16d031900b2a736e1508db4b586814e6345857f354a70ccecb1df3b50a19adaf43c278efa423ff4bb6c523ec7fd7859b97b168a7ebff8467c" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f80818283848586", + "key": "", + "out": "0602185d8c3a78738b99164b8bc6ffb21c7debebbf806372e0da44d121545597b9c662a255dc31542cf995ecbe6a50fb5e6e0ee4ef240fe557eded1188087e86" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f8081828384858687", + "key": "", + "out": "c08afa5b927bf08097afc5fff9ca4e7800125c1f52f2af3553fa2b89e1e3015c4f87d5e0a48956ad31450b083dad147ffb5ec03434a26830cf37d103ab50c5da" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788", + "key": "", + "out": "36f1e1c11d6ef6bc3b536d505d544a871522c5c2a253067ec9933b6ec25464daf985525f5b9560a16d890259ac1bb5cc67c0c469cde133def000ea1d686f4f5d" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f80818283848586878889", + "key": "", + "out": "bf2ab2e2470f5438c3b689e66e7686fffa0cb1e1798ad3a86ff99075bf6138e33d9c0ce59afb24ac67a02af34428191a9a0a6041c07471b7c3b1a752d6fc0b8b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a", + "key": "", + "out": "d400601f9728ccc4c92342d9787d8d28ab323af375ca5624b4bb91d17271fbae862e413be73f1f68e615b8c5c391be0dbd9144746eb339ad541547ba9c468a17" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b", + "key": "", + "out": "79fe2fe157eb85a038abb8ebbc647731d2c83f51b0ac6ee14aa284cb6a3549a4dcceb300740a825f52f5fb30b03b8c4d8b0f4aa67a63f4a94e3303c4eda4c02b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c", + "key": "", + "out": "75351313b52a8529298d8c186b1768666dcca8595317d7a4816eb88c062020c0c8efc554bb341b64688db5ccafc35f3c3cd09d6564b36d7b04a248e146980d4b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d", + "key": "", + "out": "e3128b1d311d02179d7f25f97a5a8bee2cc8c86303644fcd664e157d1fef00f23e46f9a5e8e5c890ce565bb6abd4302ce06469d52a5bd53e1c5a54d04649dc03" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e", + "key": "", + "out": "c2382a72d2d3ace9d5933d00b60827ed380cda08d0ba5f6dd41e29ee6dbe8ecb9235f06be95d83b6816a2fb7a5ad47035e8a4b69a4884b99e4bece58cab25d44" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "key": "", + "out": "6b1c69460bbd50ac2ed6f32e6e887cfed407d47dcf0aaa60387fe320d780bd03eab6d7baeb2a07d10cd552a300341354ea9a5f03183a623f92a2d4d9f00926af" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f90", + "key": "", + "out": "6cda206c80cdc9c44ba990e0328c314f819b142d00630404c48c05dc76d1b00ce4d72fc6a48e1469ddef609412c364820854214b4869af090f00d3c1ba443e1b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f9091", + "key": "", + "out": "7ffc8c26fbd6a0f7a609e6e1939f6a9edf1b0b066641fb76c4f9602ed748d11602496b35355b1aa255850a509d2f8ee18c8f3e1d7dcbc37a136598f56a59ed17" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192", + "key": "", + "out": "70de1f08dd4e09d5fc151f17fc991a23abfc05104290d50468882efaf582b6ec2f14f577c0d68c3ad06626916e3c86e6daab6c53e5163e82b6bd0ce49fc0d8df" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f90919293", + "key": "", + "out": "4f81935756ed35ee2058ee0c6a6110d6fac5cb6a4f46aa9411603f99965823b6da4838276c5c06bc7880e376d92758369ee7305bcec8d3cfd28ccabb7b4f0579" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f9091929394", + "key": "", + "out": "abcb61cb3683d18f27ad527908ed2d32a0426cb7bb4bf18061903a7dc42e7e76f982382304d18af8c80d91dd58dd47af76f8e2c36e28af2476b4bccf82e89fdf" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495", + "key": "", + "out": "02d261ad56a526331b643dd2186de9a82e72a58223cd1e723686c53d869b83b94632b7b647ab2afc0d522e29da3a5615b741d82852e0df41b66007dbcba90543" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f90919293949596", + "key": "", + "out": "c5832741fa30c5436823015383d297ff4c4a5d7276c3f902122066e04be5431b1a85faf73b918434f9300963d1dea9e8ac3924ef490226edeea5f743e410669f" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f9091929394959697", + "key": "", + "out": "cfaeab268cd075a5a6aed515023a032d54f2f2ff733ce0cbc78db51db4504d675923f82746d6594606ad5d67734b11a67cc6a468c2032e43ca1a94c6273a985e" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798", + "key": "", + "out": "860850f92eb268272b67d133609bd64e34f61bf03f4c1738645c17fec818465d7ecd2be2907641130025fda79470ab731646e7f69440e8367ea76ac4cee8a1df" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f90919293949596979899", + "key": "", + "out": "84b154ed29bbedefa648286839046f4b5aa34430e2d67f7496e4c39f2c7ea78995f69e1292200016f16ac3b37700e6c7e7861afc396b64a59a1dbf47a55c4bbc" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a", + "key": "", + "out": "aeeec260a5d8eff5ccab8b95da435a63ed7a21ea7fc7559413fd617e33609f8c290e64bbacc528f6c080262288b0f0a3219be223c991bee92e72349593e67638" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b", + "key": "", + "out": "8ad78a9f26601d127e8d2f2f976e63d19a054a17dcf59e0f013ab54a6887bbdffde7aaae117e0fbf3271016595b9d9c712c01b2c53e9655a382bc4522e616645" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c", + "key": "", + "out": "8934159dade1ac74147dfa282c75954fcef443ef25f80dfe9fb6ea633b8545111d08b34ef43fff17026c7964f5deac6d2b3c29dacf2747f022df5967dfdc1a0a" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d", + "key": "", + "out": "cd36dd0b240614cf2fa2b9e959679dcdd72ec0cd58a43da3790a92f6cdeb9e1e795e478a0a47d371100d340c5cedcdbbc9e68b3f460818e5bdff7b4cda4c2744" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e", + "key": "", + "out": "00df4e099b807137a85990f49d3a94315e5a5f7f7a6076b303e96b056fb93800111f479628e2f8db59aeb6ac70c3b61f51f9b46e80ffdeae25ebddb4af6cb4ee" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", + "key": "", + "out": "2b9c955e6caed4b7c9e246b86f9a1726e810c59d126cee66ed71bf015b83558a4b6d84d18dc3ff4620c2ffb722359fdef85ba0d4e2d22ecbe0ed784f99afe587" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0", + "key": "", + "out": "181df0a261a2f7d29ea5a15772715105d450a4b6c236f699f462d60ca76487feedfc9f5eb92df838e8fb5dc3694e84c5e0f4a10b761f506762be052c745a6ee8" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1", + "key": "", + "out": "21fb203458bf3a7e9a80439f9a902899cd5de0139dfd56f7110c9dec8437b26bda63de2f565926d85edb1d6c6825669743dd9992653d13979544d5dc8228bfaa" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2", + "key": "", + "out": "ef021f29c5ffb830e64b9aa9058dd660fd2fcb81c497a7e698bcfbf59de5ad4a86ff93c10a4b9d1ae5774725f9072dcde9e1f199bab91f8bff921864aa502eee" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3", + "key": "", + "out": "b3cfda40526b7f1d37569bdfcdf911e5a6efe6b2ec90a0454c47b2c046bf130fc3b352b34df4813d48d33ab8e269b69b075676cb6d00a8dcf9e1f967ec191b2c" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4", + "key": "", + "out": "b4c6c3b267071eefb9c8c72e0e2b941293641f8673cb70c1cc26ad1e73cf141755860ad19b34c2f34ed35bb52ec4507cc1fe59047743a5f0c6febde625e26091" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5", + "key": "", + "out": "57a34f2bcca60d4b85103b830c9d7952a416be5263ae429c9e5e53fe8590a8f78ec65a51109ea85dcdf7b6223f9f2b340539fad81923dbf8edabf95129e4dff6" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6", + "key": "", + "out": "9cf46662fcd61a232277b685663b8b5da832dfd9a3b8ccfeec993ec6ac415ad07e048adfe414df272770dba867da5c1224c6fd0aa0c2187d426ac647e9887361" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7", + "key": "", + "out": "5ce1042ab4d542c2f9ee9d17262af8164098935bef173d0e18489b04841746cd2f2df866bd7da6e5ef9024c648023ec723ab9c62fd80285739d84f15d2ab515a" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8", + "key": "", + "out": "8488396bd4a8729b7a473178f232dadf3f0f8e22678ba5a43e041e72da1e2cf82194c307207a54cb8156293339eaec693ff66bfcd5efc65e95e4ecaf54530abd" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9", + "key": "", + "out": "f598da901c3835bca560779037dfde9f0c51dc61c0b760fc1522d7b470ee63f5bdc6498476e86049ad86e4e21af2854a984cc905427d2f17f66b1f41c3da6f61" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aa", + "key": "", + "out": "5f93269798cf02132107337660a8d7a177354c0212eb93e555e7c37a08aef3d8dce01217011cd965c04dd2c105f2e2b6cae5e4e6bcaf09dfbee3e0a6a6357c37" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaab", + "key": "", + "out": "0ecf581d47bac9230986faabd70c2f5b80e91066f0ec55a842937882286d2ca007bb4e973b0b091d52167ff7c4009c7ab4ad38fff1dceacdb7be81ef4a452952" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabac", + "key": "", + "out": "5aeca8abe1528582b2a307b4009585498a3d467ca6101cb0c5126f9976056e9ffc123cc20c302b2a737f492c75d21f01512c90ca0541dfa56e950a321dcb28d8" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacad", + "key": "", + "out": "732fbf8f1cb2b8329263ede27858fe46f8d3354d376bcda0548e7ce1fa9dd11f85eb661fe950b543aa635ca4d3f04ede5b32d6b656e5ce1c44d35c4a6c56cff8" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadae", + "key": "", + "out": "d5e938735d63788c80100aefd18648d18cf272f69f20ff24cfe2895c088ad08b0104da1672a4eb26fc52545cc7d7a01b266cf546c403c45bd129eb41bdd9200b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf", + "key": "", + "out": "65a245b49352ee297d91af8c8be00528ac6e046dd83ac7bd465a98816dd68f3e00e1ae8f895327a7e9a8c9326598379a29c9fc91ec0c6eef08f3e2b216c11008" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0", + "key": "", + "out": "c95654b63019130ab45dd0fb4941b98aeb3af2a123913eca2ce99b3e97410a7bf8661cc7fbaa2bc1cf2b13113b1ed40a0118b88e5fffc3542759ea007ed4c58d" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1", + "key": "", + "out": "1eb262f38fa494431f017dad44c0dfb69324ac032f04b657fc91a88647bb74760f24e7c956514f0cf002990b182c1642b9b2426e96a61187e4e012f00e217d84" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2", + "key": "", + "out": "3b955aeebfa5151ac1ab8e3f5cc1e3767084c842a575d36269836e97353d41622b731dddcd5f269550a3a5b87be1e90326340b6e0e62555815d9600597ac6ef9" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3", + "key": "", + "out": "68289f6605473ba0e4f241baf7477a9885426a858f19ef2a18b0d40ef8e41282ed5526b519799e270f13881327918278755711071d8511fe963e3b5606aa3716" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4", + "key": "", + "out": "80a33787542612c38f6bcd7cd86cab460227509b1cbad5ec408a91413d51155a0476dadbf3a2518e4a6e77cc346622e347a469bf8baa5f04eb2d98705355d063" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5", + "key": "", + "out": "34629bc6d831391c4cdf8af1b4b7b6b8e8ee17cf98c70e5dd586cd99f14b11df945166236a9571e6d591bb83ee4d164d46f6b9d8ef86ff865a81bfb91b00424b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6", + "key": "", + "out": "8b7cc339163863bb4383e542b0ef0e7cf36b84ad932cdf5a80419ec9ad692e7a7e784d2c7cb3796a18b8f800035f3aa06c824100611120a7bdeb35618ccb81b7" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7", + "key": "", + "out": "4f084e4939dd5a7f5a658fad58a18a15c25c32ec1c7fd5c5c6c3e892b3971aeaac308304ef17b1c47239ea4bb398b3fd6d4528d8de8e768ae0f1a5a5c6b5c297" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8", + "key": "", + "out": "48f407a1af5b8009b2051742e8cf5cd5656669e7d722ee8e7bd202060849442168d8facc117c012bfb7bf449d99befff6a34aea203f1d8d352722be5014ec818" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9", + "key": "", + "out": "a6aa82cd1e426f9a73bfa39a29037876114655b8c22d6d3ff8b638ae7dea6b17843e09e52eb66fa1e475e4a8a3de429b7d0f4a776fcb8bdc9b9fede7d52e815f" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9ba", + "key": "", + "out": "5817027d6bdd00c5dd10ac593cd560372270775a18526d7e6f13872a2e20eab664625be7168ac4bd7c9e0ce7fc4099e0f48442e2c767191c6e1284e9b2ccea8c" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babb", + "key": "", + "out": "08e41028340a45c74e4052b3a8d6389e22e043a1adab5e28d97619450d723469b620caa519b81c14523854f619fd3027e3847bd03276e60604a80ddb4de876d6" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbc", + "key": "", + "out": "130b8420537eb07d72abda07c85acbd8b9a44f16321dd0422145f809673d30f2b5321326e2bff317ef3fef983c51c4f8ab24a325d298e34afce569a82555774c" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbd", + "key": "", + "out": "ac49b844afaa012e31c474ca263648844fd2f6307992c2f752aca02c3828965175794deee2d2ee95c61cd284f6b5a2d75e2ef2b29ee8149e77fb81447b2fd04b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbe", + "key": "", + "out": "b9d7ca81cc60bb9578e44024e5a0a0be80f27336a6a9f4e53df3999cb191280b090e2ac2d29c5baad9d71415bdc129e69aa2667af6a7fd5e189fccdcee817340" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf", + "key": "", + "out": "a755e113386572c75ced61d719706070b9146048e42a9f8cd35667a088b42f08808abdf77e618abd959afc757379ca2c00bcc1a48390fa2bff618b1e0078a613" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0", + "key": "", + "out": "a73c7debed326f1c0db0795ee7d6e3946894b826b1f8101c56c823ba17168312e7f53fc7dbe52c3e11e69852c40485e2ef182477862ea6a34ec136e2dfeea6f4" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1", + "key": "", + "out": "6cb8f9d52c56d82cac28f39ea1593e8bb2506293ac0d68376a1709b62a46df14a4ae64b2d8fab76733a1ced2d548e3f3c6fcb49d40c3d5808e449cd83d1c2aa2" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2", + "key": "", + "out": "683fa2b2369a10162c1c1c7b24bc970ee67da220564f32203f625696c0352a0b9ad96624362d952d84463c1106a2dba7a092599884b35a0b89c8f1b6a9b5a61e" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3", + "key": "", + "out": "aad9ad44610118b77d508aeb1bbcd1c1b7d0171397fb510a401bbc0ec34623670d86a2dc3c8f3ab5a2044df730256727545f0860ce21a1eac717dfc48f5d228e" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4", + "key": "", + "out": "c42578de23b4c987d5e1ac4d689ed5de4b0417f9704bc6bce969fa13471585d62c2cb1212a944f397fc9ca2c3747c3beb694ec4c5be68828dda53ef43faec6c0" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5", + "key": "", + "out": "470f00841ee8244e63ed2c7ea30e2e419897c197462ecccecf713b42a5065fff5914bc9b79affe8f6b657875e789ae213bd914cd35bd174d46e9d18bd843773d" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6", + "key": "", + "out": "34fc4213730f47a5e9a3580f643e12945cfcb31bf206f6ad450ce528da3fa432e005d6b0ecce10dca7c5995f6aacc5150e1b009e19751e8309f8859531844374" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7", + "key": "", + "out": "fb3c1f0f56a56f8e316fdf5d853c8c872c39635d083634c3904fc3ac07d1b578e85ff0e480e92d44ade33b62e893ee32343e79ddf6ef292e89b582d312502314" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8", + "key": "", + "out": "c7c97fc65dd2b9e3d3d607d31598d3f84261e9919251e9c8e57bb5f829377d5f73eabbed55c6c381180f29ad02e5be797ffec7e57bdecbc50ad3d062f0993ab0" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9", + "key": "", + "out": "a57a49cdbe67ae7d9f797bb5cc7efc2df07f4e1b15955f85dae74b76e2ecb85afb6cd9eeed8888d5ca3ec5ab65d27a7b19e578475760a045ac3c92e13a938e77" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9ca", + "key": "", + "out": "c7143fce9614a17fd653aeb140726dc9c3dbb1de6cc581b2726897ec24b7a50359ad492243be66d9edd8c933b5b80e0b91bb61ea98056006516976fae8d99a35" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacb", + "key": "", + "out": "65bb58d07f937e2d3c7e65385f9c54730b704105ccdb691f6e146d4ee8f6c086f49511035110a9ad6031fdceb943e0f9613bcb276dd40f0624ef0f924f809783" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcc", + "key": "", + "out": "e540277f683b1186dd3b5b3f61433396581a35feb12002be8c6a6231fc40ffa70f08081bc58b2d94f7649543614a435faa2d62110e13dabc7b86629b63af9c24" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccd", + "key": "", + "out": "418500878c5fbcb584c432f4285e05e49f2e3e075399a0dbfcf874ebf8c03d02bf16bc6989d161c77ca0786b05053c6c709433712319192128835cf0b660595b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdce", + "key": "", + "out": "889090dbb1944bdc9433ee5ef1010c7a4a24a8e71ecea8e12a31318ce49dcab0aca5c3802334aab2cc84b14c6b9321fe586bf3f876f19cd406eb1127fb944801" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf", + "key": "", + "out": "53b6a28910aa92e27e536fb549cf9b9918791060898e0b9fe183577ff43b5e9c7689c745b32e412269837c31b89e6cc12bf76e13cad366b74ece48bb85fd09e9" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0", + "key": "", + "out": "7c092080c6a80d672409d081d3d177106bcd63567785140719490950ae07ae8fcaabbaaab330cfbcf7374482c220af2eadeeb73dcbb35ed823344e144e7d4899" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1", + "key": "", + "out": "9ccde566d2400509181111f32dde4cd63209fe59a30c114546ad2776d889a41bad8fa1bb468cb2f9d42ca9928a7770fef8e8ba4d0c812d9a1e75c3d8d2ccd75a" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2", + "key": "", + "out": "6e293bf5d03fe43977cfe3f57ccdb3ae282a85455dca33f37f4b74f8398cc612433d755cbec412f8f82a3bd3bc4a278f7ecd0dfa9bbdc40be7a787c8f159b2df" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3", + "key": "", + "out": "c56546fb2178456f336164c18b90deffc83ae2b5a3aca77b6884d36d2c1db39501b3e65e36c758c66e3188451fdb3515ee162c001f06c3e8cb573adf30f7a101" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4", + "key": "", + "out": "6f82f89f299ebca2fe014b59bffe1aa84e88b1915fe256afb646fd8448af2b8891a7fab37a4ea6f9a50e6c317039d8cf878f4c8e1a0dd464f0b4d6ff1c7ea853" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5", + "key": "", + "out": "2b8599ff9c3d6198637ad51e57d1998b0d75313fe2dd61a533c964a6dd9607c6f723e9452ce46e014b1c1d6de77ba5b88c914d1c597bf1eae13474b4290e89b2" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6", + "key": "", + "out": "08bf346d38e1df06c8260edb1da75579275948d5c0a0aa9ed2886f8856de5417a156998758f5b17e52f101ca957a71137473dfd18d7d209c4c10d9233c93691d" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7", + "key": "", + "out": "6df2156d773114d310b63db9ee5350d77e6bcf25b05fcd910f9b31bc42bb13fe8225ebcb2a23a62280777b6bf74e2cd0917c7640b43defe468cd1e18c943c66a" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8", + "key": "", + "out": "7c7038bc13a91151828a5ba82b4a96040f258a4dfb1b1373f0d359168afb0517a20b28a12d3644046be66b8d08d8ae7f6a923ea1c00187c6d11dc502bac71305" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9", + "key": "", + "out": "bcd1b30d808fb739b987cbf154bea00da9d40380b861d4c1d6377122dadd61c0e59018b71941cfb62e00dcd70aeb9abf0473e80f0a7eca6b6dea246ab229dd2b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9da", + "key": "", + "out": "7ed4468d968530fe7ab2c33540b26d8c3bd3ed44b34fbe8c2a9d7f805b5ada0ea252eeade4fce97f89728ad85bc8bb2430b1bef2cddd32c8446e59b8e8ba3c67" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadb", + "key": "", + "out": "6d30b7c6ce8a3236c0ca2f8d728b1088ca06983a8043e621d5dcf0c537d13b08791edeb01a3cf0943ec1c890ab6e29b146a236cd46bcb9d93bf516fb67c63fe5" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdc", + "key": "", + "out": "97fe03cef31438508911bded975980a66029305dc5e3fa8ad1b4fb22fcdf5a19a733320327d8f71ccf496cb3a44a77af56e3dde73d3a5f176896cc57c9a5ad99" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdd", + "key": "", + "out": "785a9d0fbd21136dbce8fa7eafd63c9dad220052978416b31d9753eaa149097847ed9b30a65c70507eff01879149ed5cf0471d37798edc05abd56ad4a2cccb1d" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcddde", + "key": "", + "out": "ad408d2abddfd37b3bf34794c1a3371d928ed7fc8d966225333584c5665817832a37c07f0dc7cb5aa874cd7d20fe8fab8eabcb9b33d2e0841f6e200960899d95" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf", + "key": "", + "out": "97668f745b6032fc815d9579322769dccd9501a5080029b8ae826befb6742331bd9f76efeb3e2b8e81a9786b282f5068a3a2424697a77c41876b7e753f4c7767" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0", + "key": "", + "out": "26bb985f47e7fee0cfd252d4ef96bed42b9c370c1c6a3e8c9eb04ef7f7818b833a0d1f043ebafb911dc779e02740a02a44d3a1ea45ed4ad55e686c927cafe97e" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1", + "key": "", + "out": "5bfe2b1dcf7fe9b95088acedb575c19016c743b2e763bf5851ac407c9eda43715edfa48b4825492c5179593fff21351b76e8b7e034e4c53c79f61f29c479bd08" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2", + "key": "", + "out": "c76509ef72f4a6f9c9c40618ed52b2084f83502232e0ac8bdaf3264368e4d0180f6854c4abf4f6509c79caafc44cf3194afc57bd077bd7b3c9bda3d4b8775816" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3", + "key": "", + "out": "d66f2beab990e354ccb910e4e9c7ac618c7b63ef292a96b552341de78dc46d3ec8cfabc699b50af41fda39cf1b0173660923510ad67faedef5207cffe8641d20" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4", + "key": "", + "out": "7d8f0672992b79be3a364d8e5904f4ab713bbc8ab01b4f309ad8ccf223ce1034a860dcb0b00550612cc2fa17f2969e18f22e1427d254b4a82b3a03a3eb394adf" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5", + "key": "", + "out": "a56d6725bfb3de47c1414adf25fc8f0fc9846f6987722bc06366d5ca4e89722925ebbc881418844075397a0ca89842c7b9e9e07e1d9d183ebeb39e120b483bf7" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6", + "key": "", + "out": "af5e03d7fe60c67e10313344434e79485a03a758d6dce985574745763c1c5c77d4fb3e6fb12230368370993bf90feed0c5d1607524562d7c09c0c210ed393d7c" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7", + "key": "", + "out": "7a20540cc07bf72b582421fc342e82f52134b69841ec28ed189e2ea6a29dd2f82a640352d222b52f2911dc72a7dab31caadd80c6118f13c56b2a1e4373be0ea3" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8", + "key": "", + "out": "486f02c63e5467ea1fdde7e82bfacc2c1ba5d636d9f3d08b210da3f372f706ec218cc17ff60aef703bbe0c15c38ae55d286a684f864c78211ccab4178c92adba" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9", + "key": "", + "out": "1c7a5c1dedcd04a921788f7eb23361ca1953b04b9c7aec35d65ea3e4996db26f281278ea4ae666ad81027d98af57262cdbfa4c085f4210568c7e15eec7805114" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9ea", + "key": "", + "out": "9ce3fa9a860bdbd5378fd6d7b8b671c6cb7692910ce8f9b6cb4122cbcbe6ac06ca0422cef1225935053b7d193a81b9e972eb85a1d3074f14cbb5ec9f0573892d" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaeb", + "key": "", + "out": "a91187be5c371c4265c174fd4653b8ab708551f83d1fee1cc1479581bc006d6fb78fcc9a5dee1db3666f508f9780a37593ebcccf5fbed39667dc6361e921f779" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebec", + "key": "", + "out": "4625767d7b1d3d3ed2fbc674af14e0244152f2a4021fcf3311505d89bd81e2f9f9a500c3b199914db49500b3c98d03ea93286751a686a3b875daab0ccd63b44f" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebeced", + "key": "", + "out": "43dfdfe1b014fed3a2acabb7f3e9a182f2aa18019d27e3e6cdcf31a15b428e91e7b08cf5e5c376fce2d8a28ff85ab0a0a1656edb4a0a91532620096d9a5a652d" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedee", + "key": "", + "out": "279e3202be3989ba3112772585177487e4fe3ee3eab49c2f7fa7fe87cfe7b80d3e0355edff6d031e6c96c795db1c6f041880ec3824defacf9263820a8e7327de" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef", + "key": "", + "out": "ea2d066ac229d4d4b616a8bedec734325224e4b4e58f1ae6dad7e40c2da29196c3b1ea9571dacc81e87328caa0211e09027b0524aa3f4a849917b3586747ebbb" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0", + "key": "", + "out": "49f014f5c61822c899ab5cae51be4044a4495e777deb7da9b6d8490efbb87530adf293daf079f94c33b7044ef62e2e5bb3eb11e17304f8453ee6ce24f033ddb0" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1", + "key": "", + "out": "9233490344e5b0dc5912671b7ae54cee7730dbe1f4c7d92a4d3e3aab50571708db51dcf9c2944591db651db32d22935b86944969be77d5b5feae6c3840a8db26" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2", + "key": "", + "out": "b6e75e6f4c7f453b7465d25b5ac8c7196902eaa953875228c8634e16e2ae1f38bc3275304335f5989eccc1e34167d4e68d7719968fba8e2fe67947c35c48e806" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3", + "key": "", + "out": "cc14ca665af1483efbc3af80080e650d5046a3932f4f51f3fe90a0705ec25104adf07839265dc51d43401411246e474f0d5e5637af94767283d53e0617e981f4" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4", + "key": "", + "out": "230a1c857cb2e7852e41b647e90e4585d2d881e1734dc38955356e8dd7bff39053092c6b38e236e1899525647073dddf6895d64206325e7647f275567b255909" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5", + "key": "", + "out": "cbb65321ac436e2ffdab2936359ce49023f7dee7614ef28d173c3d27c5d1bffa51553d433f8ee3c9e49c05a2b883cce954c9a8093b80612a0cdd4732e041f995" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6", + "key": "", + "out": "3e7e570074337275efb51315588034c3cf0dddca20b4612e0bd5b881e7e5476d319ce4fe9f19186e4c0826f44f131eb048e65be242b1172c63badb123ab0cbe8" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7", + "key": "", + "out": "d32e9ec02d38d4e1b8249df8dcb00c5b9c68eb8922672e3505393b6a210ba56f9496e5ee0490ef387c3cdec061f06bc0382d9304cafbb8e0cd33d57029e62df2" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8", + "key": "", + "out": "8c1512466089f05b3775c262b62d22b83854a83218130b4ec91b3ccbd293d2a54302cecaab9b100c68d1e6ddc8f07cddbdfe6fdaaaf099cc09d6b725879c6369" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9", + "key": "", + "out": "91a7f61c97c2911e4c812ef71d780ad8fa788794561d08303fd1c1cb608a46a12563086ec5b39d471aed94fb0f6c678a43b8792932f9028d772a22768ea23a9b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa", + "key": "", + "out": "4f6bb222a395e8b18f6ba155477aed3f0729ac9e83e16d31a2a8bc655422b837c891c6199e6f0d75799e3b691525c581953517f252c4b9e3a27a28fbaf49644c" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafb", + "key": "", + "out": "5d06c07e7a646c413a501c3f4bb2fc38127de7509b7077c4d9b5613201c1aa02fd5f79d2745915dd57fbcb4ce08695f6efc0cb3d2d330e19b4b0e6004ea6471e" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfc", + "key": "", + "out": "b96756e57909968f14b796a5d30f4c9d671472cf82c8cfb2caca7ac7a44ca0a14c9842d00c82e337502c94d5960aca4c492ea7b0df919ddf1aada2a275bb10d4" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfd", + "key": "", + "out": "ff0a015e98db9c99f03977710aac3e658c0d896f6d71d618ba79dc6cf72ac75b7c038eb6862dede4543e145413a6368d69f5722c827ba3ef25b6ae6440d39276" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfe", + "key": "", + "out": "5b21c5fd8868367612474fa2e70e9cfa2201ffeee8fafab5797ad58fefa17c9b5b107da4a3db6320baaf2c8617d5a51df914ae88da3867c2d41f0cc14fa67928" + }, + { + "hash": "blake2b", + "in": "", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "10ebb67700b1868efb4417987acf4690ae9d972fb7a590c2f02871799aaa4786b5e996e8f0f4eb981fc214b005f42d2ff4233499391653df7aefcbc13fc51568" + }, + { + "hash": "blake2b", + "in": "00", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "961f6dd1e4dd30f63901690c512e78e4b45e4742ed197c3c5e45c549fd25f2e4187b0bc9fe30492b16b0d0bc4ef9b0f34c7003fac09a5ef1532e69430234cebd" + }, + { + "hash": "blake2b", + "in": "0001", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "da2cfbe2d8409a0f38026113884f84b50156371ae304c4430173d08a99d9fb1b983164a3770706d537f49e0c916d9f32b95cc37a95b99d857436f0232c88a965" + }, + { + "hash": "blake2b", + "in": "000102", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "33d0825dddf7ada99b0e7e307104ad07ca9cfd9692214f1561356315e784f3e5a17e364ae9dbb14cb2036df932b77f4b292761365fb328de7afdc6d8998f5fc1" + }, + { + "hash": "blake2b", + "in": "00010203", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "beaa5a3d08f3807143cf621d95cd690514d0b49efff9c91d24b59241ec0eefa5f60196d407048bba8d2146828ebcb0488d8842fd56bb4f6df8e19c4b4daab8ac" + }, + { + "hash": "blake2b", + "in": "0001020304", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "098084b51fd13deae5f4320de94a688ee07baea2800486689a8636117b46c1f4c1f6af7f74ae7c857600456a58a3af251dc4723a64cc7c0a5ab6d9cac91c20bb" + }, + { + "hash": "blake2b", + "in": "000102030405", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "6044540d560853eb1c57df0077dd381094781cdb9073e5b1b3d3f6c7829e12066bbaca96d989a690de72ca3133a83652ba284a6d62942b271ffa2620c9e75b1f" + }, + { + "hash": "blake2b", + "in": "00010203040506", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "7a8cfe9b90f75f7ecb3acc053aaed6193112b6f6a4aeeb3f65d3de541942deb9e2228152a3c4bbbe72fc3b12629528cfbb09fe630f0474339f54abf453e2ed52" + }, + { + "hash": "blake2b", + "in": "0001020304050607", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "380beaf6ea7cc9365e270ef0e6f3a64fb902acae51dd5512f84259ad2c91f4bc4108db73192a5bbfb0cbcf71e46c3e21aee1c5e860dc96e8eb0b7b8426e6abe9" + }, + { + "hash": "blake2b", + "in": "000102030405060708", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "60fe3c4535e1b59d9a61ea8500bfac41a69dffb1ceadd9aca323e9a625b64da5763bad7226da02b9c8c4f1a5de140ac5a6c1124e4f718ce0b28ea47393aa6637" + }, + { + "hash": "blake2b", + "in": "00010203040506070809", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "4fe181f54ad63a2983feaaf77d1e7235c2beb17fa328b6d9505bda327df19fc37f02c4b6f0368ce23147313a8e5738b5fa2a95b29de1c7f8264eb77b69f585cd" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "f228773ce3f3a42b5f144d63237a72d99693adb8837d0e112a8a0f8ffff2c362857ac49c11ec740d1500749dac9b1f4548108bf3155794dcc9e4082849e2b85b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "962452a8455cc56c8511317e3b1f3b2c37df75f588e94325fdd77070359cf63a9ae6e930936fdf8e1e08ffca440cfb72c28f06d89a2151d1c46cd5b268ef8563" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "43d44bfa18768c59896bf7ed1765cb2d14af8c260266039099b25a603e4ddc5039d6ef3a91847d1088d401c0c7e847781a8a590d33a3c6cb4df0fab1c2f22355" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "dcffa9d58c2a4ca2cdbb0c7aa4c4c1d45165190089f4e983bb1c2cab4aaeff1fa2b5ee516fecd780540240bf37e56c8bcca7fab980e1e61c9400d8a9a5b14ac6" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "6fbf31b45ab0c0b8dad1c0f5f4061379912dde5aa922099a030b725c73346c524291adef89d2f6fd8dfcda6d07dad811a9314536c2915ed45da34947e83de34e" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "a0c65bddde8adef57282b04b11e7bc8aab105b99231b750c021f4a735cb1bcfab87553bba3abb0c3e64a0b6955285185a0bd35fb8cfde557329bebb1f629ee93" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f10", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "f99d815550558e81eca2f96718aed10d86f3f1cfb675cce06b0eff02f617c5a42c5aa760270f2679da2677c5aeb94f1142277f21c7f79f3c4f0cce4ed8ee62b1" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f1011", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "95391da8fc7b917a2044b3d6f5374e1ca072b41454d572c7356c05fd4bc1e0f40b8bb8b4a9f6bce9be2c4623c399b0dca0dab05cb7281b71a21b0ebcd9e55670" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "04b9cd3d20d221c09ac86913d3dc63041989a9a1e694f1e639a3ba7e451840f750c2fc191d56ad61f2e7936bc0ac8e094b60caeed878c18799045402d61ceaf9" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f10111213", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "ec0e0ef707e4ed6c0c66f9e089e4954b058030d2dd86398fe84059631f9ee591d9d77375355149178c0cf8f8e7c49ed2a5e4f95488a2247067c208510fadc44c" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f1011121314", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "9a37cce273b79c09913677510eaf7688e89b3314d3532fd2764c39de022a2945b5710d13517af8ddc0316624e73bec1ce67df15228302036f330ab0cb4d218dd" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "4cf9bb8fb3d4de8b38b2f262d3c40f46dfe747e8fc0a414c193d9fcf753106ce47a18f172f12e8a2f1c26726545358e5ee28c9e2213a8787aafbc516d2343152" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f10111213141516", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "64e0c63af9c808fd893137129867fd91939d53f2af04be4fa268006100069b2d69daa5c5d8ed7fddcb2a70eeecdf2b105dd46a1e3b7311728f639ab489326bc9" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f1011121314151617", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "5e9c93158d659b2def06b0c3c7565045542662d6eee8a96a89b78ade09fe8b3dcc096d4fe48815d88d8f82620156602af541955e1f6ca30dce14e254c326b88f" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "7775dff889458dd11aef417276853e21335eb88e4dec9cfb4e9edb49820088551a2ca60339f12066101169f0dfe84b098fddb148d9da6b3d613df263889ad64b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f10111213141516171819", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "f0d2805afbb91f743951351a6d024f9353a23c7ce1fc2b051b3a8b968c233f46f50f806ecb1568ffaa0b60661e334b21dde04f8fa155ac740eeb42e20b60d764" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "86a2af316e7d7754201b942e275364ac12ea8962ab5bd8d7fb276dc5fbffc8f9a28cae4e4867df6780d9b72524160927c855da5b6078e0b554aa91e31cb9ca1d" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "10bdf0caa0802705e706369baf8a3f79d72c0a03a80675a7bbb00be3a45e516424d1ee88efb56f6d5777545ae6e27765c3a8f5e493fc308915638933a1dfee55" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "b01781092b1748459e2e4ec178696627bf4ebafebba774ecf018b79a68aeb84917bf0b84bb79d17b743151144cd66b7b33a4b9e52c76c4e112050ff5385b7f0b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "c6dbc61dec6eaeac81e3d5f755203c8e220551534a0b2fd105a91889945a638550204f44093dd998c076205dffad703a0e5cd3c7f438a7e634cd59fededb539e" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "eba51acffb4cea31db4b8d87e9bf7dd48fe97b0253ae67aa580f9ac4a9d941f2bea518ee286818cc9f633f2a3b9fb68e594b48cdd6d515bf1d52ba6c85a203a7" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "86221f3ada52037b72224f105d7999231c5e5534d03da9d9c0a12acb68460cd375daf8e24386286f9668f72326dbf99ba094392437d398e95bb8161d717f8991" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "5595e05c13a7ec4dc8f41fb70cb50a71bce17c024ff6de7af618d0cc4e9c32d9570d6d3ea45b86525491030c0d8f2b1836d5778c1ce735c17707df364d054347" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "ce0f4f6aca89590a37fe034dd74dd5fa65eb1cbd0a41508aaddc09351a3cea6d18cb2189c54b700c009f4cbf0521c7ea01be61c5ae09cb54f27bc1b44d658c82" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "7ee80b06a215a3bca970c77cda8761822bc103d44fa4b33f4d07dcb997e36d55298bceae12241b3fa07fa63be5576068da387b8d5859aeab701369848b176d42" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "940a84b6a84d109aab208c024c6ce9647676ba0aaa11f86dbb7018f9fd2220a6d901a9027f9abcf935372727cbf09ebd61a2a2eeb87653e8ecad1bab85dc8327" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021222324", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "2020b78264a82d9f4151141adba8d44bf20c5ec062eee9b595a11f9e84901bf148f298e0c9f8777dcdbc7cc4670aac356cc2ad8ccb1629f16f6a76bcefbee760" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "d1b897b0e075ba68ab572adf9d9c436663e43eb3d8e62d92fc49c9be214e6f27873fe215a65170e6bea902408a25b49506f47babd07cecf7113ec10c5dd31252" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223242526", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "b14d0c62abfa469a357177e594c10c194243ed2025ab8aa5ad2fa41ad318e0ff48cd5e60bec07b13634a711d2326e488a985f31e31153399e73088efc86a5c55" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021222324252627", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "4169c5cc808d2697dc2a82430dc23e3cd356dc70a94566810502b8d655b39abf9e7f902fe717e0389219859e1945df1af6ada42e4ccda55a197b7100a30c30a1" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "258a4edb113d66c839c8b1c91f15f35ade609f11cd7f8681a4045b9fef7b0b24c82cda06a5f2067b368825e3914e53d6948ede92efd6e8387fa2e537239b5bee" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223242526272829", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "79d2d8696d30f30fb34657761171a11e6c3f1e64cbe7bebee159cb95bfaf812b4f411e2f26d9c421dc2c284a3342d823ec293849e42d1e46b0a4ac1e3c86abaa" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "8b9436010dc5dee992ae38aea97f2cd63b946d94fedd2ec9671dcde3bd4ce9564d555c66c15bb2b900df72edb6b891ebcadfeff63c9ea4036a998be7973981e7" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "c8f68e696ed28242bf997f5b3b34959508e42d613810f1e2a435c96ed2ff560c7022f361a9234b9837feee90bf47922ee0fd5f8ddf823718d86d1e16c6090071" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "b02d3eee4860d5868b2c39ce39bfe81011290564dd678c85e8783f29302dfc1399ba95b6b53cd9ebbf400cca1db0ab67e19a325f2d115812d25d00978ad1bca4" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "7693ea73af3ac4dad21ca0d8da85b3118a7d1c6024cfaf557699868217bc0c2f44a199bc6c0edd519798ba05bd5b1b4484346a47c2cadf6bf30b785cc88b2baf" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "a0e5c1c0031c02e48b7f09a5e896ee9aef2f17fc9e18e997d7f6cac7ae316422c2b1e77984e5f3a73cb45deed5d3f84600105e6ee38f2d090c7d0442ea34c46d" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "41daa6adcfdb69f1440c37b596440165c15ada596813e2e22f060fcd551f24dee8e04ba6890387886ceec4a7a0d7fc6b44506392ec3822c0d8c1acfc7d5aebe8" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "14d4d40d5984d84c5cf7523b7798b254e275a3a8cc0a1bd06ebc0bee726856acc3cbf516ff667cda2058ad5c3412254460a82c92187041363cc77a4dc215e487" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "d0e7a1e2b9a447fee83e2277e9ff8010c2f375ae12fa7aaa8ca5a6317868a26a367a0b69fbc1cf32a55d34eb370663016f3d2110230eba754028a56f54acf57c" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "e771aa8db5a3e043e8178f39a0857ba04a3f18e4aa05743cf8d222b0b095825350ba422f63382a23d92e4149074e816a36c1cd28284d146267940b31f8818ea2" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "feb4fd6f9e87a56bef398b3284d2bda5b5b0e166583a66b61e538457ff0584872c21a32962b9928ffab58de4af2edd4e15d8b35570523207ff4e2a5aa7754caa" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "462f17bf005fb1c1b9e671779f665209ec2873e3e411f98dabf240a1d5ec3f95ce6796b6fc23fe171903b502023467dec7273ff74879b92967a2a43a5a183d33" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "d3338193b64553dbd38d144bea71c5915bb110e2d88180dbc5db364fd6171df317fc7268831b5aef75e4342b2fad8797ba39eddcef80e6ec08159350b1ad696d" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233343536", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "e1590d585a3d39f7cb599abd479070966409a6846d4377acf4471d065d5db94129cc9be92573b05ed226be1e9b7cb0cabe87918589f80dadd4ef5ef25a93d28e" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "f8f3726ac5a26cc80132493a6fedcb0e60760c09cfc84cad178175986819665e76842d7b9fedf76dddebf5d3f56faaad4477587af21606d396ae570d8e719af2" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "30186055c07949948183c850e9a756cc09937e247d9d928e869e20bafc3cd9721719d34e04a0899b92c736084550186886efba2e790d8be6ebf040b209c439a4" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233343536373839", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "f3c4276cb863637712c241c444c5cc1e3554e0fddb174d035819dd83eb700b4ce88df3ab3841ba02085e1a99b4e17310c5341075c0458ba376c95a6818fbb3e2" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "0aa007c4dd9d5832393040a1583c930bca7dc5e77ea53add7e2b3f7c8e231368043520d4a3ef53c969b6bbfd025946f632bd7f765d53c21003b8f983f75e2a6a" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "08e9464720533b23a04ec24f7ae8c103145f765387d738777d3d343477fd1c58db052142cab754ea674378e18766c53542f71970171cc4f81694246b717d7564" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "d37ff7ad297993e7ec21e0f1b4b5ae719cdc83c5db687527f27516cbffa822888a6810ee5c1ca7bfe3321119be1ab7bfa0a502671c8329494df7ad6f522d440f" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "dd9042f6e464dcf86b1262f6accfafbd8cfd902ed3ed89abf78ffa482dbdeeb6969842394c9a1168ae3d481a017842f660002d42447c6b22f7b72f21aae021c9" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "bd965bf31e87d70327536f2a341cebc4768eca275fa05ef98f7f1b71a0351298de006fba73fe6733ed01d75801b4a928e54231b38e38c562b2e33ea1284992fa" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "65676d800617972fbd87e4b9514e1c67402b7a331096d3bfac22f1abb95374abc942f16e9ab0ead33b87c91968a6e509e119ff07787b3ef483e1dcdccf6e3022" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "939fa189699c5d2c81ddd1ffc1fa207c970b6a3685bb29ce1d3e99d42f2f7442da53e95a72907314f4588399a3ff5b0a92beb3f6be2694f9f86ecf2952d5b41c" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f4041", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "c516541701863f91005f314108ceece3c643e04fc8c42fd2ff556220e616aaa6a48aeb97a84bad74782e8dff96a1a2fa949339d722edcaa32b57067041df88cc" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "987fd6e0d6857c553eaebb3d34970a2c2f6e89a3548f492521722b80a1c21a153892346d2cba6444212d56da9a26e324dccbc0dcde85d4d2ee4399eec5a64e8f" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40414243", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "ae56deb1c2328d9c4017706bce6e99d41349053ba9d336d677c4c27d9fd50ae6aee17e853154e1f4fe7672346da2eaa31eea53fcf24a22804f11d03da6abfc2b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f4041424344", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "49d6a608c9bde4491870498572ac31aac3fa40938b38a7818f72383eb040ad39532bc06571e13d767e6945ab77c0bdc3b0284253343f9f6c1244ebf2ff0df866" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "da582ad8c5370b4469af862aa6467a2293b2b28bd80ae0e91f425ad3d47249fdf98825cc86f14028c3308c9804c78bfeeeee461444ce243687e1a50522456a1d" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40414243444546", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "d5266aa3331194aef852eed86d7b5b2633a0af1c735906f2e13279f14931a9fc3b0eac5ce9245273bd1aa92905abe16278ef7efd47694789a7283b77da3c70f8" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f4041424344454647", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "2962734c28252186a9a1111c732ad4de4506d4b4480916303eb7991d659ccda07a9911914bc75c418ab7a4541757ad054796e26797feaf36e9f6ad43f14b35a4" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "e8b79ec5d06e111bdfafd71e9f5760f00ac8ac5d8bf768f9ff6f08b8f026096b1cc3a4c973333019f1e3553e77da3f98cb9f542e0a90e5f8a940cc58e59844b3" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40414243444546474849", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "dfb320c44f9d41d1efdcc015f08dd5539e526e39c87d509ae6812a969e5431bf4fa7d91ffd03b981e0d544cf72d7b1c0374f8801482e6dea2ef903877eba675e" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "d88675118fdb55a5fb365ac2af1d217bf526ce1ee9c94b2f0090b2c58a06ca58187d7fe57c7bed9d26fca067b4110eefcd9a0a345de872abe20de368001b0745" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "b893f2fc41f7b0dd6e2f6aa2e0370c0cff7df09e3acfcc0e920b6e6fad0ef747c40668417d342b80d2351e8c175f20897a062e9765e6c67b539b6ba8b9170545" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "6c67ec5697accd235c59b486d7b70baeedcbd4aa64ebd4eef3c7eac189561a726250aec4d48cadcafbbe2ce3c16ce2d691a8cce06e8879556d4483ed7165c063" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "f1aa2b044f8f0c638a3f362e677b5d891d6fd2ab0765f6ee1e4987de057ead357883d9b405b9d609eea1b869d97fb16d9b51017c553f3b93c0a1e0f1296fedcd" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "cbaa259572d4aebfc1917acddc582b9f8dfaa928a198ca7acd0f2aa76a134a90252e6298a65b08186a350d5b7626699f8cb721a3ea5921b753ae3a2dce24ba3a" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "fa1549c9796cd4d303dcf452c1fbd5744fd9b9b47003d920b92de34839d07ef2a29ded68f6fc9e6c45e071a2e48bd50c5084e96b657dd0404045a1ddefe282ed" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f50", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "5cf2ac897ab444dcb5c8d87c495dbdb34e1838b6b629427caa51702ad0f9688525f13bec503a3c3a2c80a65e0b5715e8afab00ffa56ec455a49a1ad30aa24fcd" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f5051", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "9aaf80207bace17bb7ab145757d5696bde32406ef22b44292ef65d4519c3bb2ad41a59b62cc3e94b6fa96d32a7faadae28af7d35097219aa3fd8cda31e40c275" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "af88b163402c86745cb650c2988fb95211b94b03ef290eed9662034241fd51cf398f8073e369354c43eae1052f9b63b08191caa138aa54fea889cc7024236897" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f50515253", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "48fa7d64e1ceee27b9864db5ada4b53d00c9bc7626555813d3cd6730ab3cc06ff342d727905e33171bde6e8476e77fb1720861e94b73a2c538d254746285f430" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f5051525354", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "0e6fd97a85e904f87bfe85bbeb34f69e1f18105cf4ed4f87aec36c6e8b5f68bd2a6f3dc8a9ecb2b61db4eedb6b2ea10bf9cb0251fb0f8b344abf7f366b6de5ab" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "06622da5787176287fdc8fed440bad187d830099c94e6d04c8e9c954cda70c8bb9e1fc4a6d0baa831b9b78ef6648681a4867a11da93ee36e5e6a37d87fc63f6f" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f50515253545556", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "1da6772b58fabf9c61f68d412c82f182c0236d7d575ef0b58dd22458d643cd1dfc93b03871c316d8430d312995d4197f0874c99172ba004a01ee295abac24e46" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f5051525354555657", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "3cd2d9320b7b1d5fb9aab951a76023fa667be14a9124e394513918a3f44096ae4904ba0ffc150b63bc7ab1eeb9a6e257e5c8f000a70394a5afd842715de15f29" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "04cdc14f7434e0b4be70cb41db4c779a88eaef6accebcb41f2d42fffe7f32a8e281b5c103a27021d0d08362250753cdf70292195a53a48728ceb5844c2d98bab" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f50515253545556575859", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "9071b7a8a075d0095b8fb3ae5113785735ab98e2b52faf91d5b89e44aac5b5d4ebbf91223b0ff4c71905da55342e64655d6ef8c89a4768c3f93a6dc0366b5bc8" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "ebb30240dd96c7bc8d0abe49aa4edcbb4afdc51ff9aaf720d3f9e7fbb0f9c6d6571350501769fc4ebd0b2141247ff400d4fd4be414edf37757bb90a32ac5c65a" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "8532c58bf3c8015d9d1cbe00eef1f5082f8f3632fbe9f1ed4f9dfb1fa79e8283066d77c44c4af943d76b300364aecbd0648c8a8939bd204123f4b56260422dec" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "fe9846d64f7c7708696f840e2d76cb4408b6595c2f81ec6a28a7f2f20cb88cfe6ac0b9e9b8244f08bd7095c350c1d0842f64fb01bb7f532dfcd47371b0aeeb79" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "28f17ea6fb6c42092dc264257e29746321fb5bdaea9873c2a7fa9d8f53818e899e161bc77dfe8090afd82bf2266c5c1bc930a8d1547624439e662ef695f26f24" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "ec6b7d7f030d4850acae3cb615c21dd25206d63e84d1db8d957370737ba0e98467ea0ce274c66199901eaec18a08525715f53bfdb0aacb613d342ebdceeddc3b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "b403d3691c03b0d3418df327d5860d34bbfcc4519bfbce36bf33b208385fadb9186bc78a76c489d89fd57e7dc75412d23bcd1dae8470ce9274754bb8585b13c5" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f60", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "31fc79738b8772b3f55cd8178813b3b52d0db5a419d30ba9495c4b9da0219fac6df8e7c23a811551a62b827f256ecdb8124ac8a6792ccfecc3b3012722e94463" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f6061", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "bb2039ec287091bcc9642fc90049e73732e02e577e2862b32216ae9bedcd730c4c284ef3968c368b7d37584f97bd4b4dc6ef6127acfe2e6ae2509124e66c8af4" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "f53d68d13f45edfcb9bd415e2831e938350d5380d3432278fc1c0c381fcb7c65c82dafe051d8c8b0d44e0974a0e59ec7bf7ed0459f86e96f329fc79752510fd3" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f60616263", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "8d568c7984f0ecdf7640fbc483b5d8c9f86634f6f43291841b309a350ab9c1137d24066b09da9944bac54d5bb6580d836047aac74ab724b887ebf93d4b32eca9" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f6061626364", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "c0b65ce5a96ff774c456cac3b5f2c4cd359b4ff53ef93a3da0778be4900d1e8da1601e769e8f1b02d2a2f8c5b9fa10b44f1c186985468feeb008730283a6657d" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "4900bba6f5fb103ece8ec96ada13a5c3c85488e05551da6b6b33d988e611ec0fe2e3c2aa48ea6ae8986a3a231b223c5d27cec2eadde91ce07981ee652862d1e4" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f60616263646566", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "c7f5c37c7285f927f76443414d4357ff789647d7a005a5a787e03c346b57f49f21b64fa9cf4b7e45573e23049017567121a9c3d4b2b73ec5e9413577525db45a" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f6061626364656667", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "ec7096330736fdb2d64b5653e7475da746c23a4613a82687a28062d3236364284ac01720ffb406cfe265c0df626a188c9e5963ace5d3d5bb363e32c38c2190a6" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "82e744c75f4649ec52b80771a77d475a3bc091989556960e276a5f9ead92a03f718742cdcfeaee5cb85c44af198adc43a4a428f5f0c2ddb0be36059f06d7df73" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f60616263646566676869", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "2834b7a7170f1f5b68559ab78c1050ec21c919740b784a9072f6e5d69f828d70c919c5039fb148e39e2c8a52118378b064ca8d5001cd10a5478387b966715ed6" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "16b4ada883f72f853bb7ef253efcab0c3e2161687ad61543a0d2824f91c1f81347d86be709b16996e17f2dd486927b0288ad38d13063c4a9672c39397d3789b6" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "78d048f3a69d8b54ae0ed63a573ae350d89f7c6cf1f3688930de899afa037697629b314e5cd303aa62feea72a25bf42b304b6c6bcb27fae21c16d925e1fbdac3" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "0f746a48749287ada77a82961f05a4da4abdb7d77b1220f836d09ec814359c0ec0239b8c7b9ff9e02f569d1b301ef67c4612d1de4f730f81c12c40cc063c5caa" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "f0fc859d3bd195fbdc2d591e4cdac15179ec0f1dc821c11df1f0c1d26e6260aaa65b79fafacafd7d3ad61e600f250905f5878c87452897647a35b995bcadc3a3" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "2620f687e8625f6a412460b42e2cef67634208ce10a0cbd4dff7044a41b7880077e9f8dc3b8d1216d3376a21e015b58fb279b521d83f9388c7382c8505590b9b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "227e3aed8d2cb10b918fcb04f9de3e6d0a57e08476d93759cd7b2ed54a1cbf0239c528fb04bbf288253e601d3bc38b21794afef90b17094a182cac557745e75f" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f70", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "1a929901b09c25f27d6b35be7b2f1c4745131fdebca7f3e2451926720434e0db6e74fd693ad29b777dc3355c592a361c4873b01133a57c2e3b7075cbdb86f4fc" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f7071", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "5fd7968bc2fe34f220b5e3dc5af9571742d73b7d60819f2888b629072b96a9d8ab2d91b82d0a9aaba61bbd39958132fcc4257023d1eca591b3054e2dc81c8200" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "dfcce8cf32870cc6a503eadafc87fd6f78918b9b4d0737db6810be996b5497e7e5cc80e312f61e71ff3e9624436073156403f735f56b0b01845c18f6caf772e6" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f70717273", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "02f7ef3a9ce0fff960f67032b296efca3061f4934d690749f2d01c35c81c14f39a67fa350bc8a0359bf1724bffc3bca6d7c7bba4791fd522a3ad353c02ec5aa8" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f7071727374", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "64be5c6aba65d594844ae78bb022e5bebe127fd6b6ffa5a13703855ab63b624dcd1a363f99203f632ec386f3ea767fc992e8ed9686586aa27555a8599d5b808f" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "f78585505c4eaa54a8b5be70a61e735e0ff97af944ddb3001e35d86c4e2199d976104b6ae31750a36a726ed285064f5981b503889fef822fcdc2898dddb7889a" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f70717273747576", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "e4b5566033869572edfd87479a5bb73c80e8759b91232879d96b1dda36c012076ee5a2ed7ae2de63ef8406a06aea82c188031b560beafb583fb3de9e57952a7e" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f7071727374757677", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "e1b3e7ed867f6c9484a2a97f7715f25e25294e992e41f6a7c161ffc2adc6daaeb7113102d5e6090287fe6ad94ce5d6b739c6ca240b05c76fb73f25dd024bf935" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "85fd085fdc12a080983df07bd7012b0d402a0f4043fcb2775adf0bad174f9b08d1676e476985785c0a5dcc41dbff6d95ef4d66a3fbdc4a74b82ba52da0512b74" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f70717273747576777879", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "aed8fa764b0fbff821e05233d2f7b0900ec44d826f95e93c343c1bc3ba5a24374b1d616e7e7aba453a0ada5e4fab5382409e0d42ce9c2bc7fb39a99c340c20f0" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "7ba3b2e297233522eeb343bd3ebcfd835a04007735e87f0ca300cbee6d416565162171581e4020ff4cf176450f1291ea2285cb9ebffe4c56660627685145051c" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "de748bcf89ec88084721e16b85f30adb1a6134d664b5843569babc5bbd1a15ca9b61803c901a4fef32965a1749c9f3a4e243e173939dc5a8dc495c671ab52145" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "aaf4d2bdf200a919706d9842dce16c98140d34bc433df320aba9bd429e549aa7a3397652a4d768277786cf993cde2338673ed2e6b66c961fefb82cd20c93338f" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "c408218968b788bf864f0997e6bc4c3dba68b276e2125a4843296052ff93bf5767b8cdce7131f0876430c1165fec6c4f47adaa4fd8bcfacef463b5d3d0fa61a0" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "76d2d819c92bce55fa8e092ab1bf9b9eab237a25267986cacf2b8ee14d214d730dc9a5aa2d7b596e86a1fd8fa0804c77402d2fcd45083688b218b1cdfa0dcbcb" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "72065ee4dd91c2d8509fa1fc28a37c7fc9fa7d5b3f8ad3d0d7a25626b57b1b44788d4caf806290425f9890a3a2a35a905ab4b37acfd0da6e4517b2525c9651e4" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f80", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "64475dfe7600d7171bea0b394e27c9b00d8e74dd1e416a79473682ad3dfdbb706631558055cfc8a40e07bd015a4540dcdea15883cbbf31412df1de1cd4152b91" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f8081", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "12cd1674a4488a5d7c2b3160d2e2c4b58371bedad793418d6f19c6ee385d70b3e06739369d4df910edb0b0a54cbff43d54544cd37ab3a06cfa0a3ddac8b66c89" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "60756966479dedc6dd4bcff8ea7d1d4ce4d4af2e7b097e32e3763518441147cc12b3c0ee6d2ecabf1198cec92e86a3616fba4f4e872f5825330adbb4c1dee444" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f80818283", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "a7803bcb71bc1d0f4383dde1e0612e04f872b715ad30815c2249cf34abb8b024915cb2fc9f4e7cc4c8cfd45be2d5a91eab0941c7d270e2da4ca4a9f7ac68663a" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f8081828384", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "b84ef6a7229a34a750d9a98ee2529871816b87fbe3bc45b45fa5ae82d5141540211165c3c5d7a7476ba5a4aa06d66476f0d9dc49a3f1ee72c3acabd498967414" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "fae4b6d8efc3f8c8e64d001dabec3a21f544e82714745251b2b4b393f2f43e0da3d403c64db95a2cb6e23ebb7b9e94cdd5ddac54f07c4a61bd3cb10aa6f93b49" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f80818283848586", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "34f7286605a122369540141ded79b8957255da2d4155abbf5a8dbb89c8eb7ede8eeef1daa46dc29d751d045dc3b1d658bb64b80ff8589eddb3824b13da235a6b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f8081828384858687", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "3b3b48434be27b9eababba43bf6b35f14b30f6a88dc2e750c358470d6b3aa3c18e47db4017fa55106d8252f016371a00f5f8b070b74ba5f23cffc5511c9f09f0" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "ba289ebd6562c48c3e10a8ad6ce02e73433d1e93d7c9279d4d60a7e879ee11f441a000f48ed9f7c4ed87a45136d7dccdca482109c78a51062b3ba4044ada2469" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f80818283848586878889", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "022939e2386c5a37049856c850a2bb10a13dfea4212b4c732a8840a9ffa5faf54875c5448816b2785a007da8a8d2bc7d71a54e4e6571f10b600cbdb25d13ede3" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "e6fec19d89ce8717b1a087024670fe026f6c7cbda11caef959bb2d351bf856f8055d1c0ebdaaa9d1b17886fc2c562b5e99642fc064710c0d3488a02b5ed7f6fd" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "94c96f02a8f576aca32ba61c2b206f907285d9299b83ac175c209a8d43d53bfe683dd1d83e7549cb906c28f59ab7c46f8751366a28c39dd5fe2693c9019666c8" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "31a0cd215ebd2cb61de5b9edc91e6195e31c59a5648d5c9f737e125b2605708f2e325ab3381c8dce1a3e958886f1ecdc60318f882cfe20a24191352e617b0f21" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "91ab504a522dce78779f4c6c6ba2e6b6db5565c76d3e7e7c920caf7f757ef9db7c8fcf10e57f03379ea9bf75eb59895d96e149800b6aae01db778bb90afbc989" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "d85cabc6bd5b1a01a5afd8c6734740da9fd1c1acc6db29bfc8a2e5b668b028b6b3154bfb8703fa3180251d589ad38040ceb707c4bad1b5343cb426b61eaa49c1" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "d62efbec2ca9c1f8bd66ce8b3f6a898cb3f7566ba6568c618ad1feb2b65b76c3ce1dd20f7395372faf28427f61c9278049cf0140df434f5633048c86b81e0399" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f90", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "7c8fdc6175439e2c3db15bafa7fb06143a6a23bc90f449e79deef73c3d492a671715c193b6fea9f036050b946069856b897e08c00768f5ee5ddcf70b7cd6d0e0" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f9091", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "58602ee7468e6bc9df21bd51b23c005f72d6cb013f0a1b48cbec5eca299299f97f09f54a9a01483eaeb315a6478bad37ba47ca1347c7c8fc9e6695592c91d723" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "27f5b79ed256b050993d793496edf4807c1d85a7b0a67c9c4fa99860750b0ae66989670a8ffd7856d7ce411599e58c4d77b232a62bef64d15275be46a68235ff" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f90919293", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "3957a976b9f1887bf004a8dca942c92d2b37ea52600f25e0c9bc5707d0279c00c6e85a839b0d2d8eb59c51d94788ebe62474a791cadf52cccf20f5070b6573fc" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f9091929394", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "eaa2376d55380bf772ecca9cb0aa4668c95c707162fa86d518c8ce0ca9bf7362b9f2a0adc3ff59922df921b94567e81e452f6c1a07fc817cebe99604b3505d38" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "c1e2c78b6b2734e2480ec550434cb5d613111adcc21d475545c3b1b7e6ff12444476e5c055132e2229dc0f807044bb919b1a5662dd38a9ee65e243a3911aed1a" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f90919293949596", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "8ab48713389dd0fcf9f965d3ce66b1e559a1f8c58741d67683cd971354f452e62d0207a65e436c5d5d8f8ee71c6abfe50e669004c302b31a7ea8311d4a916051" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f9091929394959697", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "24ce0addaa4c65038bd1b1c0f1452a0b128777aabc94a29df2fd6c7e2f85f8ab9ac7eff516b0e0a825c84a24cfe492eaad0a6308e46dd42fe8333ab971bb30ca" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "5154f929ee03045b6b0c0004fa778edee1d139893267cc84825ad7b36c63de32798e4a166d24686561354f63b00709a1364b3c241de3febf0754045897467cd4" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f90919293949596979899", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "e74e907920fd87bd5ad636dd11085e50ee70459c443e1ce5809af2bc2eba39f9e6d7128e0e3712c316da06f4705d78a4838e28121d4344a2c79c5e0db307a677" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "bf91a22334bac20f3fd80663b3cd06c4e8802f30e6b59f90d3035cc9798a217ed5a31abbda7fa6842827bdf2a7a1c21f6fcfccbb54c6c52926f32da816269be1" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "d9d5c74be5121b0bd742f26bffb8c89f89171f3f934913492b0903c271bbe2b3395ef259669bef43b57f7fcc3027db01823f6baee66e4f9fead4d6726c741fce" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "50c8b8cf34cd879f80e2faab3230b0c0e1cc3e9dcadeb1b9d97ab923415dd9a1fe38addd5c11756c67990b256e95ad6d8f9fedce10bf1c90679cde0ecf1be347" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "0a386e7cd5dd9b77a035e09fe6fee2c8ce61b5383c87ea43205059c5e4cd4f4408319bb0a82360f6a58e6c9ce3f487c446063bf813bc6ba535e17fc1826cfc91" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "1f1459cb6b61cbac5f0efe8fc487538f42548987fcd56221cfa7beb22504769e792c45adfb1d6b3d60d7b749c8a75b0bdf14e8ea721b95dca538ca6e25711209" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "e58b3836b7d8fedbb50ca5725c6571e74c0785e97821dab8b6298c10e4c079d4a6cdf22f0fedb55032925c16748115f01a105e77e00cee3d07924dc0d8f90659" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "b929cc6505f020158672deda56d0db081a2ee34c00c1100029bdf8ea98034fa4bf3e8655ec697fe36f40553c5bb46801644a627d3342f4fc92b61f03290fb381" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "72d353994b49d3e03153929a1e4d4f188ee58ab9e72ee8e512f29bc773913819ce057ddd7002c0433ee0a16114e3d156dd2c4a7e80ee53378b8670f23e33ef56" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "c70ef9bfd775d408176737a0736d68517ce1aaad7e81a93c8c1ed967ea214f56c8a377b1763e676615b60f3988241eae6eab9685a5124929d28188f29eab06f7" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "c230f0802679cb33822ef8b3b21bf7a9a28942092901d7dac3760300831026cf354c9232df3e084d9903130c601f63c1f4a4a4b8106e468cd443bbe5a734f45f" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "6f43094cafb5ebf1f7a4937ec50f56a4c9da303cbb55ac1f27f1f1976cd96beda9464f0e7b9c54620b8a9fba983164b8be3578425a024f5fe199c36356b88972" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "3745273f4c38225db2337381871a0c6aafd3af9b018c88aa02025850a5dc3a42a1a3e03e56cbf1b0876d63a441f1d2856a39b8801eb5af325201c415d65e97fe" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "c50c44cca3ec3edaae779a7e179450ebdda2f97067c690aa6c5a4ac7c30139bb27c0df4db3220e63cb110d64f37ffe078db72653e2daacf93ae3f0a2d1a7eb2e" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "8aef263e385cbc61e19b28914243262af5afe8726af3ce39a79c27028cf3ecd3f8d2dfd9cfc9ad91b58f6f20778fd5f02894a3d91c7d57d1e4b866a7f364b6be" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "28696141de6e2d9bcb3235578a66166c1448d3e905a1b482d423be4bc5369bc8c74dae0acc9cc123e1d8ddce9f97917e8c019c552da32d39d2219b9abf0fa8c8" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "2fb9eb2085830181903a9dafe3db428ee15be7662224efd643371fb25646aee716e531eca69b2bdc8233f1a8081fa43da1500302975a77f42fa592136710e9dc" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aa", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "66f9a7143f7a3314a669bf2e24bbb35014261d639f495b6c9c1f104fe8e320aca60d4550d69d52edbd5a3cdeb4014ae65b1d87aa770b69ae5c15f4330b0b0ad8" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaab", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "f4c4dd1d594c3565e3e25ca43dad82f62abea4835ed4cd811bcd975e46279828d44d4c62c3679f1b7f7b9dd4571d7b49557347b8c5460cbdc1bef690fb2a08c0" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabac", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "8f1dc9649c3a84551f8f6e91cac68242a43b1f8f328ee92280257387fa7559aa6db12e4aeadc2d26099178749c6864b357f3f83b2fb3efa8d2a8db056bed6bcc" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacad", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "3139c1a7f97afd1675d460ebbc07f2728aa150df849624511ee04b743ba0a833092f18c12dc91b4dd243f333402f59fe28abdbbbae301e7b659c7a26d5c0f979" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadae", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "06f94a2996158a819fe34c40de3cf0379fd9fb85b3e363ba3926a0e7d960e3f4c2e0c70c7ce0ccb2a64fc29869f6e7ab12bd4d3f14fce943279027e785fb5c29" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "c29c399ef3eee8961e87565c1ce263925fc3d0ce267d13e48dd9e732ee67b0f69fad56401b0f10fcaac119201046cca28c5b14abdea3212ae65562f7f138db3d" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "4cec4c9df52eef05c3f6faaa9791bc7445937183224ecc37a1e58d0132d35617531d7e795f52af7b1eb9d147de1292d345fe341823f8e6bc1e5badca5c656108" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "898bfbae93b3e18d00697eab7d9704fa36ec339d076131cefdf30edbe8d9cc81c3a80b129659b163a323bab9793d4feed92d54dae966c77529764a09be88db45" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "ee9bd0469d3aaf4f14035be48a2c3b84d9b4b1fff1d945e1f1c1d38980a951be197b25fe22c731f20aeacc930ba9c4a1f4762227617ad350fdabb4e80273a0f4" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "3d4d3113300581cd96acbf091c3d0f3c310138cd6979e6026cde623e2dd1b24d4a8638bed1073344783ad0649cc6305ccec04beb49f31c633088a99b65130267" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "95c0591ad91f921ac7be6d9ce37e0663ed8011c1cfd6d0162a5572e94368bac02024485e6a39854aa46fe38e97d6c6b1947cd272d86b06bb5b2f78b9b68d559d" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "227b79ded368153bf46c0a3ca978bfdbef31f3024a5665842468490b0ff748ae04e7832ed4c9f49de9b1706709d623e5c8c15e3caecae8d5e433430ff72f20eb" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "5d34f3952f0105eef88ae8b64c6ce95ebfade0e02c69b08762a8712d2e4911ad3f941fc4034dc9b2e479fdbcd279b902faf5d838bb2e0c6495d372b5b7029813" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "7f939bf8353abce49e77f14f3750af20b7b03902e1a1e7fb6aaf76d0259cd401a83190f15640e74f3e6c5a90e839c7821f6474757f75c7bf9002084ddc7a62dc" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "062b61a2f9a33a71d7d0a06119644c70b0716a504de7e5e1be49bd7b86e7ed6817714f9f0fc313d06129597e9a2235ec8521de36f7290a90ccfc1ffa6d0aee29" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "f29e01eeae64311eb7f1c6422f946bf7bea36379523e7b2bbaba7d1d34a22d5ea5f1c5a09d5ce1fe682cced9a4798d1a05b46cd72dff5c1b355440b2a2d476bc" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9ba", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "ec38cd3bbab3ef35d7cb6d5c914298351d8a9dc97fcee051a8a02f58e3ed6184d0b7810a5615411ab1b95209c3c810114fdeb22452084e77f3f847c6dbaafe16" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babb", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "c2aef5e0ca43e82641565b8cb943aa8ba53550caef793b6532fafad94b816082f0113a3ea2f63608ab40437ecc0f0229cb8fa224dcf1c478a67d9b64162b92d1" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbc", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "15f534efff7105cd1c254d074e27d5898b89313b7d366dc2d7d87113fa7d53aae13f6dba487ad8103d5e854c91fdb6e1e74b2ef6d1431769c30767dde067a35c" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbd", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "89acbca0b169897a0a2714c2df8c95b5b79cb69390142b7d6018bb3e3076b099b79a964152a9d912b1b86412b7e372e9cecad7f25d4cbab8a317be36492a67d7" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbe", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "e3c0739190ed849c9c962fd9dbb55e207e624fcac1eb417691515499eea8d8267b7e8f1287a63633af5011fde8c4ddf55bfdf722edf88831414f2cfaed59cb9a" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "8d6cf87c08380d2d1506eee46fd4222d21d8c04e585fbfd08269c98f702833a156326a0724656400ee09351d57b440175e2a5de93cc5f80db6daf83576cf75fa" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "da24bede383666d563eeed37f6319baf20d5c75d1635a6ba5ef4cfa1ac95487e96f8c08af600aab87c986ebad49fc70a58b4890b9c876e091016daf49e1d322e" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "f9d1d1b1e87ea7ae753a029750cc1cf3d0157d41805e245c5617bb934e732f0ae3180b78e05bfe76c7c3051e3e3ac78b9b50c05142657e1e03215d6ec7bfd0fc" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "11b7bc1668032048aa43343de476395e814bbbc223678db951a1b03a021efac948cfbe215f97fe9a72a2f6bc039e3956bfa417c1a9f10d6d7ba5d3d32ff323e5" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "b8d9000e4fc2b066edb91afee8e7eb0f24e3a201db8b6793c0608581e628ed0bcc4e5aa6787992a4bcc44e288093e63ee83abd0bc3ec6d0934a674a4da13838a" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "ce325e294f9b6719d6b61278276ae06a2564c03bb0b783fafe785bdf89c7d5acd83e78756d301b445699024eaeb77b54d477336ec2a4f332f2b3f88765ddb0c3" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "29acc30e9603ae2fccf90bf97e6cc463ebe28c1b2f9b4b765e70537c25c702a29dcbfbf14c99c54345ba2b51f17b77b5f15db92bbad8fa95c471f5d070a137cc" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "3379cbaae562a87b4c0425550ffdd6bfe1203f0d666cc7ea095be407a5dfe61ee91441cd5154b3e53b4f5fb31ad4c7a9ad5c7af4ae679aa51a54003a54ca6b2d" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "3095a349d245708c7cf550118703d7302c27b60af5d4e67fc978f8a4e60953c7a04f92fcf41aee64321ccb707a895851552b1e37b00bc5e6b72fa5bcef9e3fff" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "07262d738b09321f4dbccec4bb26f48cb0f0ed246ce0b31b9a6e7bc683049f1f3e5545f28ce932dd985c5ab0f43bd6de0770560af329065ed2e49d34624c2cbb" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "b6405eca8ee3316c87061cc6ec18dba53e6c250c63ba1f3bae9e55dd3498036af08cd272aa24d713c6020d77ab2f3919af1a32f307420618ab97e73953994fb4" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9ca", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "7ee682f63148ee45f6e5315da81e5c6e557c2c34641fc509c7a5701088c38a74756168e2cd8d351e88fd1a451f360a01f5b2580f9b5a2e8cfc138f3dd59a3ffc" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacb", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "1d263c179d6b268f6fa016f3a4f29e943891125ed8593c81256059f5a7b44af2dcb2030d175c00e62ecaf7ee96682aa07ab20a611024a28532b1c25b86657902" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcc", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "106d132cbdb4cd2597812846e2bc1bf732fec5f0a5f65dbb39ec4e6dc64ab2ce6d24630d0f15a805c3540025d84afa98e36703c3dbee713e72dde8465bc1be7e" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccd", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "0e79968226650667a8d862ea8da4891af56a4e3a8b6d1750e394f0dea76d640d85077bcec2cc86886e506751b4f6a5838f7f0b5fef765d9dc90dcdcbaf079f08" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdce", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "521156a82ab0c4e566e5844d5e31ad9aaf144bbd5a464fdca34dbd5717e8ff711d3ffebbfa085d67fe996a34f6d3e4e60b1396bf4b1610c263bdbb834d560816" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "1aba88befc55bc25efbce02db8b9933e46f57661baeabeb21cc2574d2a518a3cba5dc5a38e49713440b25f9c744e75f6b85c9d8f4681f676160f6105357b8406" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "5a9949fcb2c473cda968ac1b5d08566dc2d816d960f57e63b898fa701cf8ebd3f59b124d95bfbbedc5f1cf0e17d5eaed0c02c50b69d8a402cabcca4433b51fd4" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "b0cead09807c672af2eb2b0f06dde46cf5370e15a4096b1a7d7cbb36ec31c205fbefca00b7a4162fa89fb4fb3eb78d79770c23f44e7206664ce3cd931c291e5d" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "bb6664931ec97044e45b2ae420ae1c551a8874bc937d08e969399c3964ebdba8346cdd5d09caafe4c28ba7ec788191ceca65ddd6f95f18583e040d0f30d0364d" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "65bc770a5faa3792369803683e844b0be7ee96f29f6d6a35568006bd5590f9a4ef639b7a8061c7b0424b66b60ac34af3119905f33a9d8c3ae18382ca9b689900" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "ea9b4dca333336aaf839a45c6eaa48b8cb4c7ddabffea4f643d6357ea6628a480a5b45f2b052c1b07d1fedca918b6f1139d80f74c24510dcbaa4be70eacc1b06" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "e6342fb4a780ad975d0e24bce149989b91d360557e87994f6b457b895575cc02d0c15bad3ce7577f4c63927ff13f3e381ff7e72bdbe745324844a9d27e3f1c01" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "3e209c9b33e8e461178ab46b1c64b49a07fb745f1c8bc95fbfb94c6b87c69516651b264ef980937fad41238b91ddc011a5dd777c7efd4494b4b6ecd3a9c22ac0" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "fd6a3d5b1875d80486d6e69694a56dbb04a99a4d051f15db2689776ba1c4882e6d462a603b7015dc9f4b7450f05394303b8652cfb404a266962c41bae6e18a94" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "951e27517e6bad9e4195fc8671dee3e7e9be69cee1422cb9fecfce0dba875f7b310b93ee3a3d558f941f635f668ff832d2c1d033c5e2f0997e4c66f147344e02" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "8eba2f874f1ae84041903c7c4253c82292530fc8509550bfdc34c95c7e2889d5650b0ad8cb988e5c4894cb87fbfbb19612ea93ccc4c5cad17158b9763464b492" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9da", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "16f712eaa1b7c6354719a8e7dbdfaf55e4063a4d277d947550019b38dfb564830911057d50506136e2394c3b28945cc964967d54e3000c2181626cfb9b73efd2" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadb", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "c39639e7d5c7fb8cdd0fd3e6a52096039437122f21c78f1679cea9d78a734c56ecbeb28654b4f18e342c331f6f7229ec4b4bc281b2d80a6eb50043f31796c88c" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdc", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "72d081af99f8a173dcc9a0ac4eb3557405639a29084b54a40172912a2f8a395129d5536f0918e902f9e8fa6000995f4168ddc5f893011be6a0dbc9b8a1a3f5bb" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdd", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "c11aa81e5efd24d5fc27ee586cfd8847fbb0e27601ccece5ecca0198e3c7765393bb74457c7e7a27eb9170350e1fb53857177506be3e762cc0f14d8c3afe9077" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcddde", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "c28f2150b452e6c0c424bcde6f8d72007f9310fed7f2f87de0dbb64f4479d6c1441ba66f44b2accee61609177ed340128b407ecec7c64bbe50d63d22d8627727" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "f63d88122877ec30b8c8b00d22e89000a966426112bd44166e2f525b769ccbe9b286d437a0129130dde1a86c43e04bedb594e671d98283afe64ce331de9828fd" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "348b0532880b88a6614a8d7408c3f913357fbb60e995c60205be9139e74998aede7f4581e42f6b52698f7fa1219708c14498067fd1e09502de83a77dd281150c" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "5133dc8bef725359dff59792d85eaf75b7e1dcd1978b01c35b1b85fcebc63388ad99a17b6346a217dc1a9622ebd122ecf6913c4d31a6b52a695b86af00d741a0" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "2753c4c0e98ecad806e88780ec27fccd0f5c1ab547f9e4bf1659d192c23aa2cc971b58b6802580baef8adc3b776ef7086b2545c2987f348ee3719cdef258c403" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "b1663573ce4b9d8caefc865012f3e39714b9898a5da6ce17c25a6a47931a9ddb9bbe98adaa553beed436e89578455416c2a52a525cf2862b8d1d49a2531b7391" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "64f58bd6bfc856f5e873b2a2956ea0eda0d6db0da39c8c7fc67c9f9feefcff3072cdf9e6ea37f69a44f0c61aa0da3693c2db5b54960c0281a088151db42b11e8" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "0764c7be28125d9065c4b98a69d60aede703547c66a12e17e1c618994132f5ef82482c1e3fe3146cc65376cc109f0138ed9a80e49f1f3c7d610d2f2432f20605" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "f748784398a2ff03ebeb07e155e66116a839741a336e32da71ec696001f0ad1b25cd48c69cfca7265eca1dd71904a0ce748ac4124f3571076dfa7116a9cf00e9" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "3f0dbc0186bceb6b785ba78d2a2a013c910be157bdaffae81bb6663b1a73722f7f1228795f3ecada87cf6ef0078474af73f31eca0cc200ed975b6893f761cb6d" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "d4762cd4599876ca75b2b8fe249944dbd27ace741fdab93616cbc6e425460feb51d4e7adcc38180e7fc47c89024a7f56191adb878dfde4ead62223f5a2610efe" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "cd36b3d5b4c91b90fcbba79513cfee1907d8645a162afd0cd4cf4192d4a5f4c892183a8eacdb2b6b6a9d9aa8c11ac1b261b380dbee24ca468f1bfd043c58eefe" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9ea", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "98593452281661a53c48a9d8cd790826c1a1ce567738053d0bee4a91a3d5bd92eefdbabebe3204f2031ca5f781bda99ef5d8ae56e5b04a9e1ecd21b0eb05d3e1" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaeb", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "771f57dd2775ccdab55921d3e8e30ccf484d61fe1c1b9c2ae819d0fb2a12fab9be70c4a7a138da84e8280435daade5bbe66af0836a154f817fb17f3397e725a3" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebec", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "c60897c6f828e21f16fbb5f15b323f87b6c8955eabf1d38061f707f608abdd993fac3070633e286cf8339ce295dd352df4b4b40b2f29da1dd50b3a05d079e6bb" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebeced", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "8210cd2c2d3b135c2cf07fa0d1433cd771f325d075c6469d9c7f1ba0943cd4ab09808cabf4acb9ce5bb88b498929b4b847f681ad2c490d042db2aec94214b06b" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedee", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "1d4edfffd8fd80f7e4107840fa3aa31e32598491e4af7013c197a65b7f36dd3ac4b478456111cd4309d9243510782fa31b7c4c95fa951520d020eb7e5c36e4ef" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "af8e6e91fab46ce4873e1a50a8ef448cc29121f7f74deef34a71ef89cc00d9274bc6c2454bbb3230d8b2ec94c62b1dec85f3593bfa30ea6f7a44d7c09465a253" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "29fd384ed4906f2d13aa9fe7af905990938bed807f1832454a372ab412eea1f5625a1fcc9ac8343b7c67c5aba6e0b1cc4644654913692c6b39eb9187ceacd3ec" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "a268c7885d9874a51c44dffed8ea53e94f78456e0b2ed99ff5a3924760813826d960a15edbedbb5de5226ba4b074e71b05c55b9756bb79e55c02754c2c7b6c8a" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "0cf8545488d56a86817cd7ecb10f7116b7ea530a45b6ea497b6c72c997e09e3d0da8698f46bb006fc977c2cd3d1177463ac9057fdd1662c85d0c126443c10473" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "b39614268fdd8781515e2cfebf89b4d5402bab10c226e6344e6b9ae000fb0d6c79cb2f3ec80e80eaeb1980d2f8698916bd2e9f747236655116649cd3ca23a837" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "74bef092fc6f1e5dba3663a3fb003b2a5ba257496536d99f62b9d73f8f9eb3ce9ff3eec709eb883655ec9eb896b9128f2afc89cf7d1ab58a72f4a3bf034d2b4a" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "3a988d38d75611f3ef38b8774980b33e573b6c57bee0469ba5eed9b44f29945e7347967fba2c162e1c3be7f310f2f75ee2381e7bfd6b3f0baea8d95dfb1dafb1" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "58aedfce6f67ddc85a28c992f1c0bd0969f041e66f1ee88020a125cbfcfebcd61709c9c4eba192c15e69f020d462486019fa8dea0cd7a42921a19d2fe546d43d" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "9347bd291473e6b4e368437b8e561e065f649a6d8ada479ad09b1999a8f26b91cf6120fd3bfe014e83f23acfa4c0ad7b3712b2c3c0733270663112ccd9285cd9" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "b32163e7c5dbb5f51fdc11d2eac875efbbcb7e7699090a7e7ff8a8d50795af5d74d9ff98543ef8cdf89ac13d0485278756e0ef00c817745661e1d59fe38e7537" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "1085d78307b1c4b008c57a2e7e5b234658a0a82e4ff1e4aaac72b312fda0fe27d233bc5b10e9cc17fdc7697b540c7d95eb215a19a1a0e20e1abfa126efd568c7" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "4e5c734c7dde011d83eac2b7347b373594f92d7091b9ca34cb9c6f39bdf5a8d2f134379e16d822f6522170ccf2ddd55c84b9e6c64fc927ac4cf8dfb2a17701f2" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafb", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "695d83bd990a1117b3d0ce06cc888027d12a054c2677fd82f0d4fbfc93575523e7991a5e35a3752e9b70ce62992e268a877744cdd435f5f130869c9a2074b338" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfc", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "a6213743568e3b3158b9184301f3690847554c68457cb40fc9a4b8cfd8d4a118c301a07737aeda0f929c68913c5f51c80394f53bff1c3e83b2e40ca97eba9e15" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfd", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "d444bfa2362a96df213d070e33fa841f51334e4e76866b8139e8af3bb3398be2dfaddcbc56b9146de9f68118dc5829e74b0c28d7711907b121f9161cb92b69a9" + }, + { + "hash": "blake2b", + "in": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfe", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "out": "142709d62e28fcccd0af97fad0f8465b971e82201dc51070faa0372aa43e92484be1c1e73ba10906d5d1853db6a4106e0a7bf9800d373d6dee2d46d62ef2a461" + } +] \ No newline at end of file From 57082b69564f334b1d02c25e211d4e55dd2e49d3 Mon Sep 17 00:00:00 2001 From: Julien Coolen Date: Tue, 19 May 2026 10:05:49 +0100 Subject: [PATCH 08/19] Add Halo2 verifier audit notes --- docs/audit.md | 554 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 554 insertions(+) create mode 100644 docs/audit.md diff --git a/docs/audit.md b/docs/audit.md new file mode 100644 index 000000000..62c2c0054 --- /dev/null +++ b/docs/audit.md @@ -0,0 +1,554 @@ +# Halo2Verifier Static Audit Notes + +I reviewed the pasted `Halo2Verifier` and `Halo2VerifyingKey` as a static, best-effort audit of the on-chain verifier surface. I did **not** run the verifier against the Rust generator, a live EIP-2537 implementation, or sample valid/invalid proofs, so the main remaining risk is generator/native-verifier equivalence. That said, I did not see an obvious "accept invalid proof" bug in the pasted verifier logic. The largest risks are deployment/precompile assumptions, production trace instrumentation, and integration misuse. + +Prior verifier/precompile audits consistently focus on exactly these areas: verifier correctness, misuse, smart-contract vulnerabilities, privacy/integrity, and precompile semantics. For example, the Worldcoin Groth16 verifier audit lists correctness, misuse/gaming, smart-contract vulnerabilities, malicious attacks, data privacy, and information integrity as audit concerns, and prior precompile audits have found issues such as pairing return-length mismatches and missing subgroup checks. + +## Executive summary + +**No critical verifier-bypass issue found from static review.** + +The code has several strong properties: exact ABI shape checks, exact proof and instance lengths, canonical Fr checks for instances/evaluations, canonical Fp coordinate checks for proof G1 points, codehash pinning of the VK runtime, rechecking the VK codehash on every proof, and return-length checks after precompile calls. + +The main findings are: + +| ID | Severity | Finding | +| --- | ---: | --- | +| M-01 | Medium | Production verifier contains active `gas_checkpoint` `LOG1` instrumentation | +| M-02 | Medium | Deployment smoke test does not prove MCOPY support or full EIP-2537 semantic compatibility | +| M-03 | Medium / High on custom chains | Proof-point validation relies on generated "all absorbed points are later consumed by precompiles" invariant | +| M-04 | Medium | Hard-coded precompile gas caps can brick verification on target chains with different gas schedules | +| H-INT | High integration risk | Raw verifier does not bind proof meaning to an application, chain, contract, user, nullifier, or program domain | +| L-01 | Low | Invalid public instances are range-checked but failure is deferred until after transcript work | +| L-02 | Low | VK header values are not cross-checked against verifier constants after `extcodecopy` | +| L-03 | Low | Quotient VM does not assert `q_pc == q_end` after bytecode interpretation | +| I-01 | Informational | VK codehash/length should be continuously tested from deployed bytecode | +| I-02 | Informational | Accumulator encoding logic needs extensive negative tests | +| I-03 | Informational | Terminal absolute-memory Yul strategy is acceptable but brittle | + +## Findings + +### M-01: Production verifier contains active `gas_checkpoint` instrumentation + +The verifier defines: + +```solidity +function gas_checkpoint(id) { + log1(0, 0, or(shl(248, id), gas())) +} +``` + +and calls it throughout `verifyProof`. + +This means every successful proof emits many anonymous logs. More importantly, the verifier cannot be safely called through `STATICCALL`, because EVM logs are state-changing side effects in a static context. Many verifier integrations expect proof verifiers to be `view`-like and call them with `staticcall` for safety. This contract documents that it is success-or-revert and uses terminal assembly, but active trace logging still makes it behave differently from a conventional verifier. + +**Impact:** Integrations that use `staticcall` will fail even for valid proofs. Valid proofs also pay extra gas and emit unexpected logs. + +**Recommendation:** Remove `gas_checkpoint` from production builds. Keep it only behind a separate trace/gas build artifact. Once removed, make `verifyProof` `external view returns (bool)` if the target precompile calls are all `staticcall`. + +### M-02: Deployment smoke test does not prove MCOPY support or full EIP-2537 semantic compatibility + +The constructor smoke-tests EIP-2537 with identity inputs for `G1ADD`, `G1MSM`, and `PAIRING_CHECK`. That is useful, but incomplete. + +The runtime verifier later uses `mcopy`, which requires Cancun-compatible execution. The constructor does not execute an `mcopy` probe, so the verifier can be deployed on a chain/fork where construction succeeds but `verifyProof` later hits an invalid opcode. + +The identity-only EIP-2537 smoke test also does not prove that the target chain correctly validates non-identity points, subgroup membership, scalar handling, or nontrivial pairings. This matters because previous precompile audits have found high-severity subgroup-check and return-shape issues in precompile implementations. + +**Impact:** On an incompatible chain, the verifier may be permanently unusable after deployment. On a nonconforming EIP-2537 implementation, proof soundness assumptions may not hold. + +**Recommendation:** Add deployment or deployment-script checks for: + +```solidity +// Constructor-level MCOPY probe idea: +mstore(0x80, 0x1234) +mcopy(0xa0, 0x80, 0x20) +if iszero(eq(mload(0xa0), 0x1234)) { revert(0, 0) } +``` + +Also test non-identity EIP-2537 behavior in CI/deployment scripts: known valid generator operations, known off-curve points, known out-of-field coordinates, invalid subgroup points when applicable, and pairing positive/negative vectors. + +### M-03: Proof-point validation relies on generated consumption invariants + +`common_uncompressed_g1` checks only that the padded BLS12-381 coordinates are canonical Fp elements. It intentionally does **not** check curve or subgroup membership: + +```solidity +// This helper does not run an independent curve/subgroup check. +// Instead, ProtocolPlan::validate rejects generated plans where an +// absorbed proof commitment would not later be consumed by an EIP-2537 +// G1MSM or pairing path... +``` + +This is a reasonable optimization if all of the following stay true: + +1. Every absorbed proof G1 point is later consumed by an EIP-2537 operation. +2. The relevant EIP-2537 implementation validates curve and subgroup membership for every point. +3. The implementation does not skip validation for zero-scalar MSM pairs. +4. Future codegen changes cannot introduce an absorbed-but-unused point. + +From a static pass, the current pasted plan appears to route proof G1 material into final MSM/pairing paths. But this is a **generator invariant**, not a runtime assertion. + +**Impact:** If a future generator emits an absorbed point that is not consumed, or if the target precompile implementation skips validation in some cases, invalid curve/subgroup points can enter the Fiat-Shamir transcript and undermine assumptions. Severity is Medium on canonical audited EIP-2537 implementations, but High on custom or immature chains. + +**Recommendation:** Add a generated manifest and CI assertion: every `common_uncompressed_g1` call site must map to a later validation/consumption site. For extra hardening, consider an explicit validation MSM with scalar `1` for any proof G1 whose later scalar may be zero. At minimum, add target-chain conformance tests for "zero scalar with invalid point" behavior. + +### M-04: Hard-coded precompile gas caps are brittle + +The verifier uses fixed gas caps such as: + +```solidity +staticcall(575096, 0x0c, 0xa300, 0x30c0, FINAL_COM_MPTR, 0x80) +staticcall(170000, 0x0f, scratch, 0x0300, scratch, 0x20) +staticcall(62000, 0x0c, ..., 0xa0, ..., 0x80) +staticcall(50000, 0x0b, ..., 0x0100, ..., 0x80) +``` + +These are probably tuned for a specific EIP-2537 gas schedule. But if the verifier is deployed to a chain with a different gas schedule, modified precompile pricing, or less favorable implementation, valid proofs can revert. + +**Impact:** Availability failure for valid proofs on target chains/forks with different precompile gas costs. + +**Recommendation:** Prefer forwarding `gas()` where safe, or leave a larger safety margin and enforce target-chain gas compatibility through deployment tests. The deployment smoke test should include worst-case-sized calls, not only one-pair identity calls. + +### H-INT: Raw verifier does not bind proof meaning to an application domain + +The verifier proves only: + +> "This proof verifies for this VK and these public instances." + +It does not enforce application meaning: expected state root, chain ID, contract address, user address, nullifier, epoch, program ID, asset ID, or any protocol authorization. The NatSpec correctly warns about this. + +**Impact:** If an application contract treats `verifyProof(proof, instances) == true` as authorization without independently checking the public inputs, proofs can be replayed or reused across contexts. + +**Recommendation:** Never expose this raw verifier as the application authorization layer. Wrap it with an application verifier that checks all public inputs before acting. At minimum, bind: + +```text +domain_separator = hash(chainid, verifying_contract, protocol_version, vk_digest) +program_id / circuit_id +state root or commitment root +nullifier or unique action ID +recipient / caller / beneficiary when relevant +asset and amount constraints +expiry / epoch / fork identifier when relevant +``` + +This is a common audit focus for ZK systems: whether the Fiat-Shamir transcript and public statement include all required prover messages and statement data is explicitly called out in prior halo2/zkEVM audit goals. + +### L-01: Invalid public instance range-check failures are deferred + +During transcript absorption, instance values are checked with: + +```solidity +success := and(success, lt(inst_be, r)) +buf_len := common_word(buf_len, inst_be) +``` + +but `success` is not enforced until after the rest of the transcript parsing. Invalid public inputs therefore still cause the verifier to read/hash later proof material before reverting. + +**Impact:** Gas inefficiency and minor griefing if a wrapper pays for verification. + +**Recommendation:** Revert immediately after the instance loop if any public input is non-canonical: + +```solidity +if iszero(success) { revert(0, 0) } +``` + +This does not change accepted proofs. + +### L-02: VK header values are not cross-checked against verifier constants + +The VK payload contains header values: + +```text +num_instances = 19 +k = 20 +has_accumulator = 1 +acc_offset = 11 +num_acc_limbs = 7 +num_acc_limb_bits = 56 +``` + +The verifier also hardcodes these values in multiple places. Since the VK runtime is codehash-pinned, this is not attacker-controlled. But it is a generator-safety issue: if the emitted verifier constants and VK header ever diverge, failures may be confusing or, in worse cases, semantically wrong. + +**Impact:** Low direct exploitability, but high debugging cost and generator safety risk. + +**Recommendation:** After `extcodecopy`, assert the important header fields: + +```solidity +if iszero(eq(mload(NUM_INSTANCES_MPTR), 19)) { revert(0, 0) } +if iszero(eq(mload(K_MPTR), 20)) { revert(0, 0) } +if iszero(eq(mload(HAS_ACCUMULATOR_MPTR), 1)) { revert(0, 0) } +if iszero(eq(mload(ACC_OFFSET_MPTR), 11)) { revert(0, 0) } +if iszero(eq(mload(NUM_ACC_LIMBS_MPTR), 7)) { revert(0, 0) } +if iszero(eq(mload(NUM_ACC_LIMB_BITS_MPTR), 56)) { revert(0, 0) } +``` + +### L-03: Quotient VM does not assert exact program termination + +The quotient interpreter stops on: + +```solidity +for { } lt(q_pc, q_end) { } { ... } +``` + +but does not assert `q_pc == q_end` afterward. Since the quotient program is pinned in the VK runtime, proof calldata cannot directly alter it. Still, exact termination is a cheap safety invariant and catches malformed generator output. + +**Impact:** Low; generator or VK payload integrity issue, not a user-controlled issue. + +**Recommendation:** Add: + +```solidity +if iszero(eq(q_pc, q_end)) { revert(0, 0) } +if q_has_top { revert(0, 0) } // if the manifest expects empty stack +``` + +Also assert stack bounds in generator tests. + +## Positive observations + +The verifier has several good defensive choices: + +* Exact ABI shape check for dynamic `bytes proof` and `uint256[] instances`. +* Exact proof length, instance length, and calldata size checks. +* Canonical Fr checks for instances, proof evaluations, and `q_evals`. +* Canonical padded Fp coordinate checks for proof G1 points. +* Non-zero top 16-byte rejection for BLS12-381 coordinate words, preventing transcript aliases. +* VK runtime length and codehash pinning at construction and rechecking before every proof. +* VK runtime uses an `INVALID || payload` layout, preventing accidental execution of payload bytes. +* Precompile return-size checks are present. +* `scalar_inv` rejects zero before calling `modexp`. +* Public accumulator identity encoding is treated canonically and non-canonical zero-like encodings appear rejected. +* The proof parser verifies it consumed exactly the generated proof bytes. + +## Recommended test plan before production + +1. **Differential tests against the native Rust verifier** + + * Valid proofs accepted by both. + * Mutate every proof field and public input; both must reject. + * Test transcript challenge equality against native verifier at each squeeze. + +2. **ABI parser tests** + + * Wrong proof head. + * Wrong instance head. + * Short calldata. + * Trailing calldata. + * Correct proof bytes but wrong ABI offsets. + * `proof.length = 0x1e60 +/- 1`. + * `instances.length = 18`, `20`, and large values. + +3. **Canonicality tests** + + * Instance equal to `FR_MODULUS`. + * Evaluation equal to `FR_MODULUS`. + * G1 coordinate with nonzero top padding. + * G1 coordinate equal to `p`. + * G1 off-curve point. + * G1 wrong-subgroup point, if applicable to the target test harness. + * Identity point for every G1 proof slot. + +4. **Accumulator tests** + + * Canonical identity accepted only in the exact encoded form. + * Non-canonical identity-like encodings rejected. + * Unused high bits in packed limbs rejected. + * Coordinates decoding to `(0,0)` rejected unless exact accumulator identity encoding. + * `acc_offset` boundary tests. + +5. **VK deployment tests** + + * Deployed VK runtime length is exactly `0x4281`. + * Runtime byte `0` is `0xfe`. + * Runtime codehash equals `EXPECTED_VK_CODEHASH`. + * Payload header words equal verifier constants. + * `extcodecopy(vk, VK_MPTR, 1, 17024)` reproduces the expected payload. + +6. **EIP-2537 conformance tests on the target chain** + + * Non-identity `G1ADD`. + * Non-identity `G1MSM`. + * Pairing positive and negative vectors. + * Invalid coordinate rejection. + * Off-curve rejection. + * Subgroup rejection. + * Zero-scalar MSM invalid-point behavior. + * Worst-case 78-term MSM gas. + +7. **Production-build check** + + * No `gas_checkpoint` calls. + * `verifyProof` callable through `staticcall`, if the integration expects it. + * No trace logs emitted on valid proofs. + +## Bottom line + +The verifier is carefully generated and has many good checks. I would not ship it unchanged mainly because the active gas logging can break integrations, and the deployment/precompile assumptions are too important to leave to comments and identity-only smoke tests. Remove trace logging, add stronger target-chain precompile conformance tests, assert codegen invariants around absorbed G1 validation, and wrap the raw verifier with strict public-input/domain checks. + +# Additional Static Review Notes + +I did a static review of the pasted verifier/VK pair. I did **not** run the generator, compile the full source, or compare against native `midnight-proofs` test vectors, so the highest-risk remaining area is still "does this generated transcript/PCS/quotient schedule exactly match Rust?" That said, I do not see an obvious "accepts arbitrary invalid proof" bug in the pasted code. The main issues are around **assumptions that must be locked down**, **deployment/integration footguns**, and **test coverage needed for a generated verifier**. + +## Summary + +| ID | Severity | Area | Finding | +| -- | --: | --- | --- | +| 1 | Medium | Point validation / transcript | Proof G1 points are absorbed before curve/subgroup validation; safety depends on every absorbed point later being validated by EIP-2537 on the target chain. | +| 2 | Medium | Deployment | Constructor smoke-tests EIP-2537 but not `MCOPY`; deployed verifier can pass constructor and later be unusable on a non-Cancun fork/chain. | +| 3 | Low/Medium | Integration | Gas-logging render emits logs and is non-`view`; this can break verifier adapters that use `staticcall` or expect a pure/view verifier. | +| 4 | Low | Gas griefing | Non-canonical public instances are only enforced after the full transcript/proof parsing phase. | +| 5 | Low | Generated VM / VK coupling | Quotient VM safety relies on generated bytecode correctness; no runtime bounds checks protect malformed pinned bytecode. | +| 6 | Informational | Error handling | Most failures use `revert(0,0)`, making integration and production incident triage unnecessarily hard. | +| 7 | Informational | VK contract | VK runtime design is good, but should be CI-checked byte-for-byte against the verifier constants and generator manifest. | +| 8 | Informational | App-level binding | Raw verifier does not bind program semantics, chain, state roots, or authorization; integration must do this explicitly. | + +The code already addresses several common verifier classes well: strict ABI shape, exact proof/instance length, canonical scalar checks, Fp range checks for uncompressed G1 encodings, VK codehash pinning at construction and per proof, `INVALID || payload` VK runtime, final success-or-revert behavior, and public accumulator canonical decoding plus G1MSM validation. + +## 1. Absorbed proof G1 points are validated late and indirectly + +**Severity: Medium** +**Location:** `common_uncompressed_g1`, all proof commitment reads + +`common_uncompressed_g1` checks that each G1 encoding is canonical as two Fp coordinates, then immediately absorbs the 128-byte encoding into the transcript. It does **not** check that the point is on-curve or in the correct subgroup at that point. The comments say this is safe because every absorbed point is later consumed by G1MSM or pairing, whose precompiles validate curve/subgroup membership. + +That assumption is central. Prior verifier audits call out exactly these two pitfall classes: proof points must be checked on the right curves, and public inputs must not have ambiguous encodings. zkSecurity's Risc0 verifier audit explicitly lists those as the "primary pitfalls" for proof verifiers. + +For a canonical EIP-2537 implementation that validates every G1 input before MSM/pairing, this is likely fine. The risk is target-chain divergence or an optimized precompile implementation that skips validation for zero-scalar terms. Some of this verifier's MSM coefficients are Fiat-Shamir-derived and truncated; they should be nonzero except with negligible probability, but this is still an assumption worth testing directly. + +**Impact:** If an absorbed, invalid point can avoid later precompile validation, it can influence transcript challenges without being bound to a valid group element. That is a classic Fiat-Shamir verifier soundness hazard. + +**Recommendation:** Add negative tests on the exact deployment target/fork: + +```text +- malformed affine coordinate, scalar 1 in G1MSM: must revert/fail +- malformed affine coordinate, scalar 0 in G1MSM: must revert/fail +- malformed F_COM, PI, advice, lookup, permutation, quotient commitment: verifyProof must revert +- identity points in each proof position: accepted/rejected exactly as native verifier does +``` + +If the target precompile does not validate zero-scalar inputs, either validate all absorbed proof points immediately with scalar 1, or ensure the generator never emits a path where an absorbed point can have zero contribution before being validated elsewhere. + +## 2. Constructor checks EIP-2537 but not `MCOPY` + +**Severity: Medium** +**Location:** `require_eip2537_precompiles`, runtime use of `mcopy` + +The constructor smoke-tests G1ADD, G1MSM, and pairing. That is good. But the verifier runtime also depends on Cancun `MCOPY` via Yul `mcopy`. The constructor does not execute `mcopy`, so a chain/fork that supports the BLS precompile addresses but not `MCOPY` can deploy the contract and later fail at verification time. + +The NatSpec says deploy only on chains/forks that support MCOPY and EIP-2537, but the constructor currently only enforces the EIP-2537 half. + +**Impact:** Deployment can succeed while every proof verification later reverts. This is a liveness/deployment safety issue, not a proof-soundness issue. + +**Recommendation:** Add a constructor-time `MCOPY` smoke test: + +```solidity +assembly ("memory-safe") { + mstore(0x80, 0x1234) + mcopy(0xa0, 0x80, 0x20) + if iszero(eq(mload(0xa0), 0x1234)) { revert(0, 0) } +} +``` + +Also include deployment CI against the exact chain/fork config, not only a local EVM. + +## 3. Gas-logging render breaks `view`/`staticcall` verifier expectations + +**Severity: Low/Medium** +**Location:** `gas_checkpoint`, `verifyProof` + +`verifyProof` emits many `LOG1` checkpoints. This makes the function non-`view` in practice and incompatible with `staticcall`. Many verifier adapters and application contracts assume proof verifiers are `view` or call them via `staticcall`. + +**Impact:** Integration failure or unexpected reverts in consuming contracts. If this render accidentally reaches production, it also permanently emits gas-left telemetry logs on every successful verification. + +**Recommendation:** Keep the gas render as a separate artifact. Production verifier should remove `gas_checkpoint` entirely and mark `verifyProof` as `external view returns (bool)` if all remaining operations are static. At minimum, make the production generator fail if gas logging is enabled. + +## 4. Public instance canonicality failure is deferred too long + +**Severity: Low** +**Location:** transcript instance absorption + +The instance loop does: + +```yul +success := and(success, lt(inst_be, r)) +buf_len := common_word(buf_len, inst_be) +``` + +but does not revert until after all proof commitments, challenges, evaluations, q-evals, `f_com`, and `pi` are parsed. This is not a soundness bug because the verifier eventually reverts, but malformed public inputs can force the contract through a large amount of transcript work first. + +The Risc0 audit's public-input finding is directly relevant: reducing or accepting multiple representations creates ambiguity; the recommendation was to assert that public inputs are already reduced, not reduce them. Your verifier correctly asserts `< r`; it just enforces the result late. + +**Impact:** Caller-pays gas griefing. Usually low, but more relevant if a relayer, rollup inbox, or batch processor subsidizes verification attempts. + +**Recommendation:** Revert immediately after the instance loop: + +```yul +if iszero(success) { revert(0, 0) } +``` + +You can still keep the exact transcript behavior for valid inputs. + +## 5. Quotient VM safety relies entirely on pinned generated bytecode + +**Severity: Low** +**Location:** quotient VM loop, `q_pc`, `q_end`, stack operations + +The compact quotient VM is pinned by the VK codehash, so proof calldata cannot alter control flow. That is a strong design choice. However, malformed generated bytecode would not be sandboxed robustly: + +```yul +let q_op := byte(0, mload(q_pc)) +q_pc := add(q_pc, 1) +... +case 0x06 { + q_sp := sub(q_sp, 0x20) + q_top := addmod(mload(q_sp), q_top, r) +} +``` + +The VM trusts the generator/manifest for stack safety, operand bounds, constant-table bounds, and exact `q_pc == q_end`. That is acceptable for generated code, but it should be treated as a formal generator invariant, not as an informal assumption. + +Trail of Bits' Axiom Halo2 review highlights the importance of generated-verifier memory conventions and tests, including the prior finding that a Solidity loader did not respect Solidity's free memory pointer. They recommended starting allocation at `0x80` and asserting the free memory pointer assumptions; the fix review later notes this was resolved by starting at `0x80` and checking `mload(0x40) == 0x80`. + +**Recommendation:** Add generator-level checks that prove or assert: + +```text +- q_pc ends exactly at q_end +- no instruction operand reads beyond q_end +- every PUSH_MEM pointer is within an initialized Fr slot +- every constant index is within q_const table +- stack depth never underflows/overflows its reserved range +- native callback indexes are exactly the generated set +- selector power indexes are within the precomputed y-power table +``` + +For production defense in depth, add a post-loop runtime check: + +```yul +if iszero(eq(q_pc, q_end)) { revert(0, 0) } +if q_has_top { revert(0, 0) } // if expression boundaries require empty stack +``` + +only if that matches the VM semantics. + +## 6. Empty reverts make production failures hard to diagnose + +**Severity: Informational** +**Location:** most `revert(0,0)` paths + +Most failures revert with no selector or data. That is gas-efficient, but it makes it difficult to distinguish malformed ABI, bad VK, non-canonical scalar, invalid G1, precompile failure, quotient VM failure, and pairing failure. + +Least Authority made a similar recommendation in the Worldcoin Groth16 verifier audit: optimized verifier code should use accurate custom errors instead of opaque reverts to improve usage and debugging. + +**Recommendation:** For production, consider a debug build with custom errors and a release build with empty reverts. At minimum, keep off-chain test harnesses that map failing mutations to sections. + +## 7. VK contract design is good, but needs byte-level CI guarantees + +**Severity: Informational** +**Location:** `Halo2VerifyingKey` + +The `INVALID || payload` runtime is a good pattern: direct calls cannot execute arbitrary payload bytes, and the verifier copies from byte 1. The length relationship is correct in the pasted constants: + +```text +VK return length: 0x4281 = 17025 +payload length: 0x4280 = 17024 +verifier expected len: 17025 +``` + +The risk is not the design; it is generator drift. If the verifier constants, VK constructor return length, codehash, q-program offsets, or fixed/permutation commitment offsets diverge, the system becomes undeployable or unsound-by-construction. + +**Recommendation:** Add CI tests that deploy the VK and assert: + +```text +- address(vk).code.length == EXPECTED_VK_LENGTH +- address(vk).codehash == EXPECTED_VK_CODEHASH +- extcodecopy byte 1..end equals the generator payload bytes exactly +- VK header words match verifier-rendered constants: + num_instances = 19 + k = 20 + has_accumulator = 1 + acc_offset = 11 + num_acc_limbs = 7 + num_acc_limb_bits = 56 +- q_program_mptr + q_program_len lands before fixed_comms +- all fixed/permutation/G2 VK points are accepted by target EIP-2537 precompiles +``` + +## 8. Raw verifier must not be treated as application authorization + +**Severity: Informational / Integration-critical** +**Location:** `verifyProof` API + +The contract correctly warns that application contracts must bind the meaning of instances separately. This is important enough to repeat: a raw proof verifier only says "this proof verifies under this VK for these public inputs." It does not enforce chain ID, state root freshness, nullifier uniqueness, program ID, account authorization, or bridge domain. + +Silent Protocol's audit makes a related point: users benefit from being able to check that open-sourced circuits compile down to the verifier keys used on-chain, in addition to assurance about the circuit statement itself. + +**Recommendation:** The consuming contract should enforce a typed public-input schema, for example: + +```text +instances[0] = protocol/domain separator +instances[1] = chain id or fork/domain id +instances[2] = application/verifier version +instances[3] = expected state root / IVC output root +instances[4] = nullifier / replay tag +... +``` + +and reject proofs whose public inputs do not match the current application state. + +## Positive observations + +The following parts look notably strong: + +* Exact ABI head checks and exact calldata size checks. +* Exact proof length and instance count checks. +* Canonical `< r` checks for proof evaluations, q-evals, and public instances. +* Canonical Fp encoding checks for uncompressed G1 calldata: top padding bytes zero and coordinates `< p`. +* VK length and codehash pinned both in the constructor and on every proof. +* VK runtime starts with `INVALID`, and verifier copies from byte 1. +* Public accumulator decoding is stricter than accepting any zero-like encoding. +* Public accumulator points are routed through G1MSM with scalar 1 before use. +* Final success path is terminal and returns ABI `true`; failures revert. +* Memory starts at `0x80` and the main assembly block is terminal, which avoids the exact Solidity free-memory-pointer issue Trail of Bits previously found in an EVM verifier loader. + +## Production readiness checklist + +Before shipping, I would require these tests/proofs: + +```text +1. Native equivalence + - Same VK, same proof, same instances accepted by Rust and Solidity. + - Mutate every proof byte/word: Rust rejects iff Solidity rejects. + - Transcript challenge snapshots match Rust after every squeeze. + +2. Encoding/canonicality + - instance = r, r+1, 2^256-1 reverts. + - eval/q_eval = r reverts. + - G1 hi padding nonzero reverts. + - G1 x or y = p reverts. + - malformed-but-in-field non-curve points revert. + +3. Accumulator public inputs + - canonical identity accepted only in exact encoding. + - non-canonical zero-like encodings rejected. + - invalid affine point rejected. + - wrong accumulator equation rejected. + - randomized batching cannot be bypassed with two bad equations in fuzz tests. + +4. Target precompile behavior + - G1MSM validates invalid points even with scalar 0. + - G1MSM validates invalid points with scalar 1. + - pairing validates invalid G1/G2. + - gas limits are sufficient on the deployment chain. + +5. Generator invariants + - all memory regions non-overlapping by generated manifest. + - quotient VM stack safety and operand bounds. + - q_program length and q_end exactness. + - final MSM term count equals input length / 0xa0. + - every absorbed proof G1 is consumed by a validating precompile path. + +6. Deployment invariants + - VK codehash/length matches constants. + - MCOPY smoke test passes. + - production build has gas logging disabled. +``` + +My highest-priority changes would be: **add MCOPY smoke test**, **remove gas checkpoints for production**, **fail public-input canonicality immediately**, and **add target-chain negative tests proving all absorbed G1 points are actually validated by EIP-2537 even in zero-scalar MSM cases**. From ff288682dd9699c72b55c07b511a583068b7b2e1 Mon Sep 17 00:00:00 2001 From: Julien Coolen Date: Tue, 19 May 2026 10:14:31 +0100 Subject: [PATCH 09/19] Update Moonlight Sepolia production verifier --- .../Halo2Verifier.runtime.bytecode.hex | 2 +- .../sepolia/moonlight-wrap/Halo2Verifier.sol | 50 +------------------ .../sepolia/moonlight-wrap/README.md | 12 ++--- .../sepolia/moonlight-wrap/deployment.json | 18 +++---- 4 files changed, 17 insertions(+), 65 deletions(-) diff --git a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.runtime.bytecode.hex b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.runtime.bytecode.hex index fc5f8ad6a..9cc24914b 100644 --- a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.runtime.bytecode.hex +++ b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.runtime.bytecode.hex @@ -1 +1 @@ -0x6108e06040526004361015610012575f80fd5b5f3560e01c80631e8e1e1314610078576342b7259714610030575f80fd5b34610074575f366003190112610074576040517f0000000000000000000000002132e1c5f2afe710e0f3fe01ec70e95000f998826001600160a01b03168152602090f35b5f80fd5b346100745760403660031901126100745760043567ffffffffffffffff8111610074573660238201121561007457806004013567ffffffffffffffff81116100745760243691830101116100745760243567ffffffffffffffff8111610074573660238201121561007457806004013567ffffffffffffffff8111610074576024369160051b8301011161007457611ec060409114911416156100745760017f0000000000000000000000002132e1c5f2afe710e0f3fe01ec70e95000f998825a8260f81b175f80a17f24430c63ec2017f12ec4004d66c66314f418dd77fe38dd4c2475c8f2b259e009813f14614281823b1416156100745781612700614280923c6121443614611ec435601314604435611e6014831616168015610074575f90731a0111ea397fe69a4b1ba7b6434bacd764774b846120a435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa612084351416731a0111ea397fe69a4b1ba7b6434bacd764774b8461206435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa6120443514161680614d3c575b15614ca8575b166080616bc061a1c05e808261a24052614c8d575b5f90731a0111ea397fe69a4b1ba7b6434bacd764774b8461212435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa612104351416731a0111ea397fe69a4b1ba7b6434bacd764774b846120e435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa6120c43514161680614c70575b15614bdc575b1680916080616c4061a1c05e61a24052614bc1575b8015610074575a600160f91b175f80a160806127005190525f60a0525f60c0525f60e0525f61010052601361012052610140611ee45b6121448110614b9d57505a600360f81b175f80a16064906191c0905b826107e48110614b80575f5160206152d85f395f51905f5285925a600160fa1b175f80a1607f190160802080608052066169805260a090619940916101008201925b838310614b625784835f5160206152d85f395f51905f52845a600560f81b175f80a1607f190160802080608052066169a0525f5160206152d85f395f51905f52602060802080608052066169c05260a0619a4061030083015b808410614b405750505a600360f91b175f80a1619d40608083015b808410614b1e575061010060806103f3858095614e62565b948181619e4037019201905b818310614afc5750505f5160206152d85f395f51905f526080610423838095614e62565b928181619ec03701915a600760f81b175f80a1607f190160802080608052066169e05260a0610100619f409301925b838310614ade5784835f5160206152d85f395f51905f52845a600160fb1b175f80a1607f19016080208060805206616a005260a090619fc0916102008201925b838310614ac05784835f5160206152d85f395f51905f52845a600960f81b175f80a1607f19016080208060805206616a205260a090618500610cc08201905b818310614a8f5750505f5160206152d85f395f51905f528192607f19016080208060805206616a40525f5160206152d85f395f51905f5260206080208060805206616a6052610120608061052483614dcc565b928181616ac0370191607f190160802092836080526001600160801b035f5160206152d85f395f51905f5260a0950616616a8052826182a052015b808210614a6757506080905f5160206152d85f395f51905f52611ec493607f1901832080845206616aa05261059381614dcc565b508181616b40370103610074578015610074575a600560f91b175f80a1616a205180915f5b60148110614a4c57506127805192616cc0846127c0515b6170608310614a2757505050506106085f5160206152d85f395f51905f525f5160206153385f395f51905f528408918261706052614ef6565b925f5160206152d85f395f51905f52616cc092612760519009916127c0515b6170608210614a02578585616ce05190616d00915b616e0083106149e5575f92611ee4905b61214482106149c057505061706051616cc05190616e005193616cc052616ce052616d0052616d2052616d4052616d60525a600b60f81b175f80a1801561007457616a0051610300525f61a300525f5b61014081106149b157506001805b603182106149885782618b4051618a40516185205190618a6051916185405161088052618a8051936185605194618aa0516108c0526185805190618ac0516185a0516108a052618ae05191886185c0519586946108805189618b0051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f529109955f5160206152d85f395f51905f529109936108a0515f5160206152d85f395f51905f52910992866108c051905f5160206152d85f395f51905f529109925f5160206152d85f395f51905f52910990610880515f5160206152d85f395f51905f52908c09905f5160206152d85f395f51905f528a8c095f5160206152d85f395f51905f529108905f5160206152d85f395f51905f529108905f5160206152d85f395f51905f529108905f5160206152d85f395f51905f529108905f5160206152d85f395f51905f529108905f5160206152d85f395f51905f529108905f5160206152d85f395f51905f5291088684618b2051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f5291085f5160206152d85f395f51905f5290600109956103005161a30051905f5160206152d85f395f51905f5291099661a1c051905f5160206152d85f395f51905f52910861a1c0526108a0515f5160206152d85f395f51905f52035f5160206152d85f395f51905f52905f08915f5160206152d85f395f51905f52035f5160206152d85f395f51905f52905f089061088051905f5160206152d85f395f51905f529108905f5160206152d85f395f51905f529108905f5160206152d85f395f51905f5291085f5160206152d85f395f51905f529060010961a1e051905f5160206152d85f395f51905f52910861a1e0525f5160206152d85f395f51905f52035f5160206152d85f395f51905f52905f08915f5160206152d85f395f51905f529108905f5160206152d85f395f51905f5291085f5160206152d85f395f51905f529060010961a20051905f5160206152d85f395f51905f529108906185e0515f5160206152d85f395f51905f52035f5160206152d85f395f51905f52905f089061088051905f5160206152d85f395f51905f529108905f5160206152d85f395f51905f5291085f5160206152d85f395f51905f5290600109918261a9605261030051906103005190610300515f5160206152d85f395f51905f529109905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f52910961a3005261a360515f5160206152d85f395f51905f529109905f5160206152d85f395f51905f52910861a20052614120905f9061a960825b84906152ef861015611f3b57600186515f1a9601959182600514611f0f575081600614611eef5781600814611ed45781600d14611eaa5781601014611e865781601114611e625781602114611bc35750806019146118785780601f146113225780601b14610b6357600b14610af2575f80fd5b600384519401935f905f5160206152d85f395f51905f526103005161a300510961a300525f5160206152d85f395f51905f5285611fe08360f31c1661a1c0019283519061ffff8160e81c16610b4b575b50089052610a7f565b90621fffe0849260e31c1661a3400151900989610b42565b505082516002909301925f925061a96090839060f01c8015610e8f5780600114610d635780600214610c7e57600314610b9a575f80fd5b5f5160206152d85f395f51905f528080808080618b0051816185e05181035f0890088180618520518161858051918009097f404d21073985d14e432a4ad76d3fae06ca74314b950fe7b1d7f501cd31a8b374099008818061854051816185a051918009097f0b2cc8704264c6bd81bc620e9e524d4b73e9b2317679422ff7fa1603955649f10990088180618560518161862051918009097f0fdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f099008600109808452816103005161a300510961a300528161a2e05161a3605190090861a2e052610a7f565b505f5160206152d85f395f51905f528080808080618ae051816185c05181035f0890088180618520518161858051918009097f1b8114c381b922fd5d6d241210e2d8a68ad5744053ba9e776118de4107b51ace099008818061854051816185a051918009097f3df32e4cc4cb2ed20e5d21899cf5331775990ccaec4c09b4e3717213fcc0d7630990088180618560518161862051918009097f3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc1099008600109808452816103005161a300510961a300528161a2e05161a3605190090861a2e052610a7f565b505f5160206152d85f395f51905f528080807f73eda753299d7d483339d80809a1d7f7b67900f7fe6bfad98998021b7bb5732b81807f73eda753299d7d483339d80809a1d80553bca402fffe5bfeffffffff0000000181808080600160701b61852051088180600160701b6185405108600160381b0990088180600160701b6185605108600160701b099008818080618660518161868051600160381b0990086186a0518290600160701b09900881035f08900808818070241eabfffeb153ffffb9feffffffffaaab6187c0510981035f089008088180600160861b817f73eda753299d7d483339d80809a1d80553bda402fffe5b6e855000003ab000026187e051080981035f089008600109808452816103005161a300510961a3005261a240510861a24052610a7f565b5061852051610400526185c051610520526185e051610480526186005161044052618540516104a0526187a0516104c0525f5160206152d85f395f51905f528080807f73eda753299d7d483339d80809a1d7d340dd972492de594de627fffefcb803498180618560516187805161054052618580516105805261876051610460526185a0516104205261874051610500526186205161056052618640516104e05281808080618660518161868051600160381b0990086186a0518290600160701b09900881035f089181808061044051600160701b09818061048051600160381b096105205108089181808083600160701b0981806104a051600160381b09610400510808918180806104c0516104e05109700c8557e86f90d0d89eed6eb5349a0f88200991818080610540516104e05109702cb9b546d20373eaf85e8f53db883cb5480991818080610460516104e0510970013af65741744bd7bb2c6872df2b8003200991818080610500516104e0510970340f2ebe380a0f5eff4360543988a61dc20991818080610440516104e0510970297784894e27525bc342b7fde37dba93660991818080610480516104e05109703212e00cde6d2002b119d800000347fcb809918180806104c0516105605109702cb9b546d20373eaf85e8f53db883cb548099181808061054051610560510970013af65741744bd7bb2c6872df2b800320099181808061046051610560510970340f2ebe380a0f5eff4360543988a61dc2099181808061050051610560510970297784894e27525bc342b7fde37dba93660991818080610440516105605109703212e00cde6d2002b119d800000347fcb809918180806104c051610420510970013af65741744bd7bb2c6872df2b800320099181808061054051610420510970340f2ebe380a0f5eff4360543988a61dc2099181808061046051610420510970297784894e27525bc342b7fde37dba93660991818080610500516104205109703212e00cde6d2002b119d800000347fcb809918180806104c051610580510970340f2ebe380a0f5eff4360543988a61dc2099181808061054051610580510970297784894e27525bc342b7fde37dba93660991818080610460516105805109703212e00cde6d2002b119d800000347fcb809918180806104c051840970297784894e27525bc342b7fde37dba936609918180808080610540518609703212e00cde6d2002b119d800000347fcb80993610520519009600160701b098180806104c0516104a05109703212e00cde6d2002b119d800000347fcb809818080610480516104a05109600160701b09818080610520516104a05109600160381b09818080610440516104005109600160701b09818080610480516104005109600160381b09816105205161040051090808080808080808080808080808080808080808080808080808080808818070241eabfffeb153ffffb9feffffffffaaab6187c0510981035f089008088180600160861b817f73eda753299d7d483339d80809a1d80553b9202d7ffe85d4800008bb200000016187e051080981035f089008600109808452816103005161a300510961a3005261a220510861a22052610a7f565b505090505f905f61a96090616d40516103a052616d005161032052616d20516103e0526169a05161038052616980516103c0525f5160206152d85f395f51905f52806190e05181610320516103a0510809816103005161a30051090861a300525f5160206152d85f395f51905f52618b6051816103c05191816103c0515f0908095f5b6004811061184b57505060015f5b600481106118295750600161a9e05260015b600481106117f75750600161aac0526003805b6117c557505f905f5b600481106117985750905f5160206152d85f395f51905f52808080808096816190c05197810391880908816103005161a3005109089381808080618be051948180618b8051816103c0515f090881618ba051916103c05190090895096190e0510881036191005108816190a0519361038051900890090881806103e05161032051088103600108099161030051900908610360526191605161034052618a00516102e05261852051610260526185405161028052618560516102c052618580516102a0526185a0516102405261862051610220526186405161020052618660516101e052618680516101c0526186a0516101a0526186c051610180526186e0516101605261870051610140526187205161012052618bc051610100526191405160e0525f5160206152d85f395f51905f5280618c005181035f0860010860c0525f5160206152d85f395f51905f52808060e05160010961034051088103619180510860a0525f5160206152d85f395f51905f5280806191205181806103805181806101005160c05109816103c05181806101205160c05109816103c05181806101405160c05109816103c05181806101605160c05109816103c05181806101805160c05109816103c05181806101a05160c05109816103c05181806101c05160c05109816103c05181806101e05160c05109816103c05181806102005160c05109816103c05181806102205160c05109816103c05181806102405160c05109816103c05181806102a05160c05109816103c05181806102c05160c05109816103c05181806102805160c05109816103c05181806102605160c05109816103c05181806102e05160c05109816103c0515f09080908090809080908090809080908090809080908090809080908090809080860a051090881806103e0516103205108810360010809816103005181805f5160206153385f395f51905f528180610380518161010051816103c0518161012051816103c0518161014051816103c0518161016051816103c0518161018051816103c051816101a051816103c051816101c051816103c051816101e051816103c0518161020051816103c0518161022051816103c0518161024051816103c051816102a051816103c051816102c051816103c0518161028051816103c0518161026051816103c051816102e051816103c0515f09080908090809080908090809080908090809080908090809080908090809080860e0510908816103005181806103405181610320516103a051080981610300516103605109080908090861a30052610a7f565b915f5160206152d85f395f51905f52600191818560051b8061aa6001519061a9e0015109900892016113e1565b5f5160206152d85f395f51905f528160051b808601519061aa600151095f19820160051b61aa6001525f1901806113d8565b6001905f5160206152d85f395f51905f525f19820160051b808701519061a9e00151098160051b61a9e00152016113c5565b905f5160206152d85f395f51905f526001918360051b860151900991016113b3565b8060019160051b5f5160206152d85f395f51905f52610380518183618540015187080890860152016113a5565b5050618a205161a9609081525f925082805b60058110611baa57506185005161aa2052616d605161aa40525f5b60098110611b915750618a005161ab80525f5b60128110611b7857505f5b60068110611b5d57505f5b60068110611b4257505f5b60058110611b2757505f5160206152d85f395f51905f5280808061ade0518103600108616d405109816103005161a30051090881808061ae80518181810391800908616d005109916103005190090861a3005260015b60068110611ae157505f5160206152d85f395f51905f52616a20516169a0510961b000525f5b600681106119635750610a7f565b600381026003810160128111611ad9575b8260051b908161aea001519161ade001519061b00051908194906169c051906169a0515b8a828510611a0c57505050505050600193925f5160206152d85f395f51905f52808080957f4285088329c399ea457a8ca1d30f8957e74c7f529842a1579b4fee55b398292395820390088180616d2051616d0051088103890809816103005161a30051090861a300520961b0005201611955565b9285979385969792939482809760051b809301519261aba001515f5160206152d85f395f51905f529087095f5160206152d85f395f51905f52908408905f5160206152d85f395f51905f5291085f5160206152d85f395f51905f529109985f5160206152d85f395f51905f529108905f5160206152d85f395f51905f5291085f5160206152d85f395f51905f529109947f08634d0aa021aaf843cab354fabb0062f6502437c6a09c006c083479590189d75f5160206152d85f395f51905f52910993600101929190611998565b506012611974565b805f5160206152d85f395f51905f52808060019460051b61ade001515f19850160051b61af60015182039008616d405109816103005161a30051090861a300520161192f565b80606060019202618ec001518160051b61af600152016118d9565b80606060019202618ea001518160051b61aea00152016118ce565b80606060019202618e8001518160051b61ade00152016118c3565b8060019160051b80618c4001519061aba00152016118b8565b8060019160051b8061862001519061aa600152016118a5565b8060019160051b8061852001519061a98001520161188a565b92939480915090600181515f1a91015f9260018316611e52575b505f9360028316611e39575b815196875f1a8860011a908960021a9a60058b60041a960199611e2b575b505f5b818110611dde5750505f5b818110611d7f5750505f5b878a821015611cb75788519860118a60f01c9101995f5b60078110611c4b5750505050600101611c20565b8060051b8301515f6080525b600760805110611c6a5750600101611c37565b9a5f5160206152d85f395f51905f5290818d81600460805187018a0101515f1a60051b612ae001519160805160051b61ffff8960e01c16015190090990089a600160805101608052611c57565b5050959750955f905b8060031a8210611d455750505f905b808210611d01575050600116611ce9575b50916001610a7f565b905f5160206152d85f395f51905f5291510984611ce0565b90935f5160206152d85f395f51905f526001918160058b519b019a81815f1a60051b612ae001519161ffff808260d81c16519160e81c165109099008940190611ccf565b90945f5160206152d85f395f51905f526001918160038c519c019b61ffff8160e81c1651905f1a60051b612ae00151099008950190611cc0565b6002895160f01c990198515f905b8a60078310611da157505050600101611c15565b600191929a5f5160206152d85f395f51905f5260038193519e019d8161ffff825f1a60051b612ae001519260e81c16518709099008990190611d8d565b5f5b8a60078210611df3575050600101611c0a565b6001919a5f5160206152d85f395f51905f5260038193519e019d61ffff8160e81c1651905f1a60051b612ae001510990089901611de0565b84526020909301928b611c07565b9350600181515f1a91019060051b612ae0015193611be9565b905160f01c925060030187611bdd565b935f5160206152d85f395f51905f5291506002865160f01c96019551900992610a7f565b935f5160206152d85f395f51905f5291506002865160f01c96019551900892610a7f565b935f5160206152d85f395f51905f529150600186515f1a96019560051b612ae00151900992610a7f565b935f5160206152d85f395f51905f52809250035f0892610a7f565b93915f5160206152d85f395f51905f529150601f19019182510892610a7f565b92949150946003905160f01c920194611f2d575b5051916001610a7f565b835260209092019184611f23565b836169e051610840525f5160206152d85f395f51905f52618ae051815f5160206153385f395f51905f526185c051099008610720526185205161082052618540516108005261856051610760525f5160206152d85f395f51905f526107605161076051096107e05261858051610860525f5160206152d85f395f51905f52610860516108605109610740526185a0516107a0525f5160206152d85f395f51905f526107a0516107a051096107805261862051610700525f5160206152d85f395f51905f526107005161070051096107c052618640516106e0525f5160206152d85f395f51905f526106e0516106e051096106c052618660516106a0525f5160206152d85f395f51905f526106a0516106a05109610680525f5160206152d85f395f51905f52618b0051815f5160206153385f395f51905f526185e05109900861066052618b205161064052618b405161062052618a405161060052618a60516105e052618a80516105c0525f5160206152d85f395f51905f52618aa051815f5160206153385f395f51905f52618600510990086105a0525f5160206152d85f395f51905f5280808080618c205181036001086191a0519009810381808080806106805161068051096106a051095f5160206152f85f395f51905f5209818080806106c0516106c051096106e051095f5160206153585f395f51905f5209818080806107c0516107c0510961070051095f5160206152b85f395f51905f5209818080806107805161078051096107a051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e098180808061074051610740510961086051097ed94c46a8456352aa44d7a885ab59e3a36664e6fb25e826f8a4cd79822f053309818080806107e0516107e0510961076051097f1f61345b652161410c5e29f51e301ae56342af824bc110649393d2b911c50d3e098180610800517f40fa389feb2522bb934881ac9ed749aee2296502af592418c6b5675c0f560261098180610820517f70d8f2a733a64d650faccc9b1c2a766a9544bb3ff1a11ee73cb43947ef386633096105a0510808080808080808816108405181808080806106c0516106c051096106e051095f5160206152f85f395f51905f5209818080806107c0516107c0510961070051095f5160206153585f395f51905f5209818080806107805161078051096107a051095f5160206152b85f395f51905f52098180808061074051610740510961086051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e09818080806107e0516107e0510961076051097ed94c46a8456352aa44d7a885ab59e3a36664e6fb25e826f8a4cd79822f0533098180610800517f27e7119226c42a6d19c1541904b99ae40685511ed2e078964b74594d38340849098180610820517f6d05a41959f539a7fc9ec0972ea1e3dbb6fc67dd51daf3414f7fbbb091c7274a0981805f5160206153385f395f51905f526106a051096105c0510808080808080808816108405181808080806107c0516107c0510961070051095f5160206152f85f395f51905f5209818080806107805161078051096107a051095f5160206153585f395f51905f52098180808061074051610740510961086051095f5160206152b85f395f51905f5209818080806107e0516107e0510961076051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e098180610800517f23a6684b942d726a22e4d5b8d8ff83aeaa773f62600184efe5d033d7c7c6e827098180610820517f2f5908b169c6cf1bd26dcf0f9e5105481f5164f3ece0582bf3098312167751a70981805f5160206153385f395f51905f526106e051096105e05108080808080808816108405181808080806107805161078051096107a051095f5160206152f85f395f51905f52098180808061074051610740510961086051095f5160206153585f395f51905f5209818080806107e0516107e0510961076051095f5160206152b85f395f51905f52098180610800517f24822e1af9aa2887c912c87eb0f20bd332330e7e55cd784de67cb407a9f05520098180610820517f726df1506749848155630b86ae25a82b281ecd050fe3a52d85a181fa87202e4b0981805f5160206153385f395f51905f526107005109610600510808080808088161084051818080808061074051610740510961086051095f5160206152f85f395f51905f5209818080806107e0516107e0510961076051095f5160206153585f395f51905f52098180610800517f26c2cc87f95726b28f33ca03409a460ec987cfe12adae32769e3565865d07191098180610820517f222e83e70453dfee19b402e9fa8dfe2c4987b034d0be3ceb478b3022e97934c10981805f5160206153385f395f51905f526107a05109610620510808080808816108405181808080806107e0516107e0510961076051095f5160206152f85f395f51905f52098180610800517f6bd72f9cfc53af9d931896e77ea5c61244cb6d5fae8954f37dc7b9002f5aa78a098180610820517f5e1d3dbecda6214343e24a47f45c5d033197ad01b65a730af95dc57e90c491400981805f5160206153385f395f51905f5261086051096106405108080808816108405181808080806106805161068051096106a051097f0fdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f09818080806106c0516106c051096106e051097f301cf56f9b4577112cc4241cddf6484aaadedbf1bbd0f2351adf2e41c2fb2ecd09818080806107c0516107c0510961070051097f5f3a15bab4ce4097b1edc3a25002694b92395ce355a8a12fe557459d9633f70109818080806107805161078051096107a051097f275a20361ea91992193920270d3e2d1f6361880ac0a439c64bef815d4469ba85098180808061074051610740510961086051097f31e823a45e567484c1544e310c0fa5cd66547a8f0dde659ac61698c30e838d2509818080806107e0516107e0510961076051097f26cc223e16f47c20e17cc6069605fa5a8af05ea4f6eb36029a641d23b818eb10098180610800517f4d0ea7f9c3fda06d9535b0fdafd8338bd47c2200b284fa71a325ff41ac358028098180610820517f5b1fc262a28cbb8bf75d9b1a6edaa74591ec24cd9a209512213cec3a3c0f1a5d09610660510808080808080808816108405181808080806106805161068051096106a051097f3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc109818080806106c0516106c051096106e051097f06ccb1c7d87f3c12a2bde4e68ac7f1e8b03481ba15d7f88f9a7f9b8310dd6d3409818080806107c0516107c0510961070051097f53fded36d490ba6b05a5d10fd99ffe5456baec6a6a8753199d5ebdc33c99790e09818080806107805161078051096107a051097f412c98232b6ab8a47aa76ee814ef7ec6261987c9802f2cfc490e007951a60ca5098180808061074051610740510961086051097f333f8046ece5579cbd6872449c57f2703dfc8864cfadc06d587ff104a0d0c1f209818080806107e0516107e0510961076051097f3509dd2fe3aac0080783557fec090fb1cb4b2b0901253c55282024331d1fe1a8098180610800517f52f789e4afc3801f7411102ee2f47cc5954a744e71cac98e75ea962a55a0a76f098180610820517f590ba402032e82eb1f660ef09796c5686345a5054ed96dae8e2d2336337887710961072051080808080808080881610840515f0908090809080908090809080908090808816103005161a3005109088161a9405161a1c0510961a1c0528161a9205161a1e0510961a1e0528161a8c05161a200510961a200528161a8605161a220510961a220528161a8005161a240510961a240528161a7a05161a260510961a260528161a7405161a280510961a280528161a6e05161a2a0510961a2a0528161a6805161a2c0510961a2c0528161a5c05161a2e0510961a2e05281035f08616d80525a600360fa1b175f80a1616a2051908160015f5b6014811061496557505f5160206152d85f395f51905f5280808080888180988198616da0528103600108616dc0525a600d60f81b175f80a181808061278051816127a051809c81809c81809c818c819d9b829c9a839b61704052820961706052098061702052090909090909090909617000525a601160f81b175f80a1616a4051600161738052600190617380905f915b602a831061493957845a600960f91b175f80a1618a0061a3005261850061a320526190a061a340526190c061a3605261912061a3805261914061a3a0526191a061a3c052618a2061a3e052618a4061a40052618a6061a42052618a8061a44052618aa061a46052618ac061a48052618ae061a4a052618b0061a4c052618b2061a4e052618b4061a50052618b6061a52052618b8061a54052618ba061a56052618bc061a58052618be061a5a052618c0061a5c052618c2061a5e052618c4061a60052618c6061a62052618c8061a64052618ca061a66052618cc061a68052618ce061a6a052618d0061a6c052618d2061a6e052618d4061a70052618d6061a72052618d8061a74052618da061a76052618dc061a78052618de061a7a052618e0061a7c052618e2061a7e052618e4061a80052618e6061a82052616d8061a84052618a00516173a061a32060015b602b811061490e57505050617ba0525a601360f81b175f80a16186e0515f5160206152d85f395f51905f526189a0519181806173a051928184618700510990089381836189c05109900882806173c051958187618720510990089181866189e05109900890617bc052617be0525a600560fa1b175f80a1818080619060518180619080519281886190e051099008956191005109900892818661916051099008936191805109900890617c0052617c20525a601560f81b175f80a161852061a300526185c061a3205261884061a3405261854061a360526185e061a3805261886061a3a05261856061a3c05261860061a3e05261888061a4005261858061a4205261874061a440526188a061a460526185a061a4805261876061a4a0526188c061a4c05261862061a4e05261878061a500526188e061a5205261864061a540526187a061a5605261890061a5805261866061a5a0526187c061a5c05261892061a5e05261868061a600526187e061a6205261894061a640526186a061a6605261880061a6805261896061a6a0526186c061a6c05261882061a6e05261898061a70052618520516185c05161884051916173a061a36060015b600b81106148c557505050617c4052617c6052617c80525a600b60f91b175f80a1618e8061a30052618ea061a32052618ec061a34052618ee061a36052618f0061a38052618f2061a3a052618f4061a3c052618f6061a3e052618f8061a40052618fa061a42052618fc061a44052618fe061a4605261900061a4805261902061a4a05261904061a4c052618e8051618ea051618ec051916173a061a36060015b6005811061487c57505050617ca052617cc052617ce0525a601760f81b175f80a1616a6051616a80516182a0516170005191836170205181617040519581617060519188815f5160206152d85f395f51905f52035f5160206152d85f395f51905f529089085f5160206152d85f395f51905f528581038a085f5160206152d85f395f51905f528381038b08905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f529109915f5160206152d85f395f51905f5281810383085f5160206152d85f395f51905f5286810384085f5160206152d85f395f51905f5290600109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52908409895f5160206152d85f395f51905f5283810388085f5160206152d85f395f51905f5285810389085f5160206152d85f395f51905f5290600109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f5290830994878d5f5160206152d85f395f51905f5282810387085f5160206152d85f395f51905f5288810388085f5160206152d85f395f51905f5290600109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f529089096130bd90614d59565b955f5160206152d85f395f51905f5283810382085f5160206152d85f395f51905f5289810383085f5160206152d85f395f51905f5290600109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52908809935f5160206152d85f395f51905f5282810385085f5160206152d85f395f51905f528a810386085f5160206152d85f395f51905f5290600109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f529086095f5160206152d85f395f51905f528381038b085f5160206152d85f395f51905f528681038c085f5160206152d85f395f51905f5290600109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52908209995f5160206152d85f395f51905f5286810389085f5160206152d85f395f51905f528281038a08905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52908c099a8b945f5160206152d85f395f51905f52035f5160206152d85f395f51905f52908a085f5160206152d85f395f51905f529109905f5160206152d85f395f51905f52035f5160206152d85f395f51905f529089085f5160206152d85f395f51905f529082099788965f5160206152d85f395f51905f52035f5160206152d85f395f51905f5291085f5160206152d85f395f51905f529109915f5160206152d85f395f51905f52910981617ca051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f5203935f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52916080013509905f5160206152d85f395f51905f529108925f5160206152d85f395f51905f52910990617cc051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52035f5160206152d85f395f51905f529108925f5160206152d85f395f51905f52910990617ce051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52035f5160206152d85f395f51905f5291085f5160206152d85f395f51905f52825f09905f5160206152d85f395f51905f52910887895f5160206152d85f395f51905f5285810388085f5160206152d85f395f51905f5282810389085f5160206152d85f395f51905f5290600109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f528881038b085f5160206152d85f395f51905f528781038c085f5160206152d85f395f51905f528481038d08905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291098a5f5160206152d85f395f51905f528a810385085f5160206152d85f395f51905f5289810386085f5160206152d85f395f51905f5290600109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f5290830991885f5160206152d85f395f51905f528c810382085f5160206152d85f395f51905f5287810383085f5160206152d85f395f51905f5290600109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52908509968c5f5160206152d85f395f51905f52878a0961358490614d59565b965f5160206152d85f395f51905f52908809935f5160206152d85f395f51905f5282810385085f5160206152d85f395f51905f528a810386085f5160206152d85f395f51905f5290600109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f529086095f5160206152d85f395f51905f528381038b085f5160206152d85f395f51905f528681038c085f5160206152d85f395f51905f5290600109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52908209995f5160206152d85f395f51905f5286810389085f5160206152d85f395f51905f528281038a08905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52908c099a8b945f5160206152d85f395f51905f52035f5160206152d85f395f51905f52908a085f5160206152d85f395f51905f529109905f5160206152d85f395f51905f52035f5160206152d85f395f51905f529089085f5160206152d85f395f51905f529082099788965f5160206152d85f395f51905f52035f5160206152d85f395f51905f5291085f5160206152d85f395f51905f529109915f5160206152d85f395f51905f52910981617c4051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f5203935f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52916060013509905f5160206152d85f395f51905f529108925f5160206152d85f395f51905f52910990617c6051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52035f5160206152d85f395f51905f529108925f5160206152d85f395f51905f52910990617c8051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52035f5160206152d85f395f51905f529108915f5160206152d85f395f51905f529109905f5160206152d85f395f51905f529108905f5160206152d85f395f51905f5281810389085f5160206152d85f395f51905f52906001095f5160206152d85f395f51905f5282810388085f5160206152d85f395f51905f528a81038908905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5289810382085f5160206152d85f395f51905f52906001095f5160206152d85f395f51905f5290830961393790614d59565b5f5160206152d85f395f51905f528a810383085f5160206152d85f395f51905f52906001095f5160206152d85f395f51905f52908209915f5160206152d85f395f51905f528181038c085f5160206152d85f395f51905f52906001095f5160206152d85f395f51905f52908409905f5160206152d85f395f51905f528c81038b085f5160206152d85f395f51905f529083099384925f5160206152d85f395f51905f528381038d085f5160206152d85f395f51905f529109915f5160206152d85f395f51905f52035f5160206152d85f395f51905f52908c085f5160206152d85f395f51905f528e81038d08905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52910981617c0051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f5203915f5160206152d85f395f51905f5291095f5160206152d85f395f51905f529060408c013509905f5160206152d85f395f51905f529108925f5160206152d85f395f51905f52910990617c2051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52035f5160206152d85f395f51905f529108915f5160206152d85f395f51905f529109905f5160206152d85f395f51905f529108905f5160206152d85f395f51905f5281810387085f5160206152d85f395f51905f52906001095f5160206152d85f395f51905f5282810386085f5160206152d85f395f51905f528881038708905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52828209925f5160206152d85f395f51905f5289810382085f5160206152d85f395f51905f52906001095f5160206152d85f395f51905f52908509613bd890614d59565b915f5160206152d85f395f51905f528a810383085f5160206152d85f395f51905f52906001095f5160206152d85f395f51905f52908409935f5160206152d85f395f51905f52908509935f5160206152d85f395f51905f528b81038a085f5160206152d85f395f51905f529086099485935f5160206152d85f395f51905f52035f5160206152d85f395f51905f52908b085f5160206152d85f395f51905f529109915f5160206152d85f395f51905f52910981617bc051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f5203915f5160206152d85f395f51905f5291095f5160206152d85f395f51905f529060208a013509905f5160206152d85f395f51905f529108925f5160206152d85f395f51905f52910990617be051905f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291095f5160206152d85f395f51905f52035f5160206152d85f395f51905f529108915f5160206152d85f395f51905f529109905f5160206152d85f395f51905f529108925f5160206152d85f395f51905f52035f5160206152d85f395f51905f529108613d9990614d59565b90617ba0515f5160206152d85f395f51905f52039035905f5160206152d85f395f51905f529108905f5160206152d85f395f51905f529109915f5160206152d85f395f51905f529109905f5160206152d85f395f51905f5291089081616e40525a600360fb1b175f80a1616aa05180808094616da051616dc051916182a051925f5160206152d85f395f51905f52856001096001600160801b038116955f5160206152d85f395f51905f5291096001600160801b038116965f5160206152d85f395f51905f5291096001600160801b038116995f5160206152d85f395f51905f5291096001600160801b038116975f5160206152d85f395f51905f5291096001600160801b03169260806198c061a3005e600161a38052608061994061a3a05e6173c05161a420526080619d4061a4405e6173e05161a4c05260806199c061a4e05e6174005161a560526080619dc061a5805e6174205161a600526080619f4061a6205e6174405161a6a052608061578061a6c05e6174605161a74052608061550061a7605e6174805161a7e052608061558061a8005e6174a05161a88052608061560061a8a05e6174c05161a92052608061568061a9405e6174e05161a9c052608061570061a9e05e6175005161aa6052608061530061aa805e6175205161ab0052608061538061ab205e6175405161aba052608061540061abc05e6175605161ac4052608061548061ac605e6175805161ace052608061580061ad005e6175a05161ad8052608061588061ada05e6175c05161ae2052608061590061ae405e6175e05161aec052608061598061aee05e6176005161af60526080615b8061af805e6176205161b000526080615f0061b0205e6176405161b0a052608061600061b0c05e6176605161b14052608061608061b1605e6176805161b1e052608061610061b2005e6176a05161b28052608061618061b2a05e6176c05161b32052608061620061b3405e6176e05161b3c052608061628061b3e05e6177005161b46052608061630061b4805e6177205161b50052608061638061b5205e6177405161b5a052608061640061b5c05e6177605161b64052608061648061b6605e6177805161b6e052608061650061b7005e6177a05161b78052608061658061b7a05e6177c05161b82052608061660061b8405e6177e05161b8c052608061668061b8e05e6178005161b96052608061670061b9805e6178205161ba0052608061678061ba205e6178405161baa052608061680061bac05e6178605161bb4052608061688061bb605e6178805161bbe052608061690061bc005e6178a05161bc80526178c051915f5160206152d85f395f51905f529083096080619fc061bca05e818161bd20525f5160206152d85f395f51905f529109608061a04061bd405e818161bdc0525f5160206152d85f395f51905f52910990608061a0c061bde05e8161be6052608061a14061be805e5f5160206152d85f395f51905f52910961bf00526080615a0061bf205e61a1c0515f5160206152d85f395f51905f5290820961bfa0526080615a8061bfc05e61a1e0515f5160206152d85f395f51905f5290820961c040526080615b0061c0605e61a200515f5160206152d85f395f51905f5290820961c0e0526080615c0061c1005e61a220515f5160206152d85f395f51905f5290820961c180526080615c8061c1a05e61a240515f5160206152d85f395f51905f5290820961c220526080615d0061c2405e61a260515f5160206152d85f395f51905f5290820961c2c0526080615d8061c2e05e61a280515f5160206152d85f395f51905f5290820961c360526080615e0061c3805e61a2a0515f5160206152d85f395f51905f5290820961c400526080615e8061c4205e61a2c0515f5160206152d85f395f51905f5290820961c4a0526080615f8061c4c05e61a2e0515f5160206152d85f395f51905f52910961c54052608061974061c5605e8361c5e05260806197c061c6005e836173a051905f5160206152d85f395f51905f52910961c68052608061984061c6a05e836173c051905f5160206152d85f395f51905f52910961c720526080619cc061c7405e8461c7c0526080619e4061c7e05e846173a051905f5160206152d85f395f51905f52910961c860526080619ec061c8805e846173c051905f5160206152d85f395f51905f52910961c9005260806191c061c9205e8761c9a052608061924061c9c05e876173a051905f5160206152d85f395f51905f52910961ca405260806192c061ca605e876173c051905f5160206152d85f395f51905f52910961cae052608061934061cb005e876173e051905f5160206152d85f395f51905f52910961cb805260806193c061cba05e8761740051905f5160206152d85f395f51905f52910961cc2052608061944061cc405e8761742051905f5160206152d85f395f51905f52910961ccc05260806194c061cce05e8761744051905f5160206152d85f395f51905f52910961cd6052608061954061cd805e8761746051905f5160206152d85f395f51905f52910961ce005260806195c061ce205e8761748051905f5160206152d85f395f51905f52910961cea052608061964061cec05e876174a051905f5160206152d85f395f51905f52910961cf405260806196c061cf605e876174c051905f5160206152d85f395f51905f52910961cfe0526080619a4061d0005e8561d080526080619ac061d0a05e856173a051905f5160206152d85f395f51905f52910961d120526080619b4061d1405e856173c051905f5160206152d85f395f51905f52910961d1c0526080619bc061d1e05e856173e051905f5160206152d85f395f51905f52910961d260526080619c4061d2805e8561740051905f5160206152d85f395f51905f52910961d300526080616ac061d3205e868261d3a052614853575b5f5160206152d85f395f51905f5296979387809693948180808080809a8199099c60808601350999606085013509966040840135099360208301350990350808080808616e60525a601960f81b175f80a16080616b40616f005e6080612860815e805f5160206152d85f395f51905f52616e605181035f086101005261483c575b806080616e806101005e614824575b6080616b406101005e80616a80516101805261480b575b806147f3575b608080616f805e5a600760f91b175f80a17c70616972696e672d62617463682d6163632d6b7a670000000000000000610100526080616f806101205e6080616f006101a05e6080616c406102205e6080616bc06102a05e5f5160206152d85f395f51905f5261022061010020069081156147ea575b6080616c406101005e8082610180526147d1575b806080616f806101805e6147b7575b80916080616bc06101005e6101805261479e575b806080616f006101805e614784575b5a600f60f81b175f80a180156100745761476e9061501d565b505a600160fc1b175f80a1600160805260206080f35b506080616f0061010080600b61c350fa60803d1416614755565b50608061010060a081600c61f230fa60803d1416614746565b506080616f8061010080600b61c350fa60803d1416614732565b50608061010060a081600c61f230fa60803d1416614723565b6001915061470f565b5060808061010081600b61c350fa60803d141661469a565b50608061010060a081600c61f230fa60803d1416614694565b5060808061010081600b61c350fa60803d141661467d565b5060808060a081600c61f230fa60803d141661466e565b9692955090926080616e806130c061a300600c6208c678fa3d60801416959296939190936145ed565b909194606060205f5160206152d85f395f51905f526001938180808c9b9a9b519b818d8c515109900899818c878c01515109900899604089015151099008970193019101612ef4565b909194606060205f5160206152d85f395f51905f526001938180808c9b9a9b519b818d8c515109900899818c878c01515109900899604089015151099008970193019101612e54565b9091926020805f5160206152d85f395f51905f52600193818851885151099008950193019101612c9c565b5f5160206152d85f395f51905f52826020600193019509926001600160801b0384168552019192612b4f565b91905f5160206152d85f395f51905f528086818460019509099280099201612abe565b5f5160206152d85f395f51905f5260019161030051900991828160051b61a340015201906106aa565b5f61a1c082015260200161069c565b909360205f5160206152d85f395f51905f52819281883586510990089501910161064c565b5f5160206152d85f395f51905f526020918451900892019161063c565b5f5160206152d85f395f51905f52838282806020958751098809855209910190610627565b602091815f5160206152d85f395f51905f5280938103870885520991019085906105cf565b915f5160206152d85f395f51905f52816001920992016105b8565b9181355f5160206152d85f395f51905f5281101561007457815260209081019291019061055f565b90928235915f5160206152d85f395f51905f52831015610074578281529181526020908101939281019291016104d1565b91906080614acf838293614e62565b93818482370191019190610492565b91906080614aed838293614e62565b93818482370191019190610452565b9190926080614b0c838293614e62565b938184823701910192909291926103ff565b916080614b2f858293969496614e62565b9481848237019101919291926103db565b916080614b51858293969496614e62565b9481848237019101919291926103c0565b91906080614b71838293614e62565b93818482370191019190610367565b614b8e608092918392614e62565b92818582370192019190610325565b90602080918335945f5160206152d85f395f51905f52861016948152019101610309565b506080616c4060a061a1c0600c61f230fa60803d14166102d3565b9050614bf2600160381b600760386120c46150ff565b9392614c08600160381b60076038612104615248565b5093919092169580614c4c575b15614c24575b505050506102be565b909192948383178287171715151694616c4052616c6052616c8052616ca05283808080614c1b565b95838317828617171516955f616c40525f616c60525f616c80525f616ca052614c15565b915082915f616c40525f616c60525f616c80525f616ca0526102b8565b506080616bc060a061a1c0600c61f230fa60803d1416610235565b9050614cbe600160381b600760386120446150ff565b9392614cd4600160381b60076038612084615248565b5093919092169580614d18575b15614cf0575b50505050610220565b909192948383178287171715151694616bc052616be052616c0052616c205283808080614ce7565b95838317828617171516955f616bc0525f616be0525f616c00525f616c2052614ce1565b915082915f616bc0525f616be0525f616c00525f616c205261021a565b801561007457602061260052602061262052602061264052612660527f73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffffff612680525f5160206152d85f395f51905f526126a052602061260060c08160055afa156100745760203d03610074576126005190565b80356040820135908060801c610074578160801c610074576001600160801b038091169116906f1a0111ea397fe69a4b1ba7b6434bacd75f5160206153185f395f51905f52602085013511158183141691101715610074576f1a0111ea397fe69a4b1ba7b6434bacd75f5160206153185f395f51905f526060840135111581831416911017156100745760809060a03761012090565b81356040830135908060801c610074578160801c610074576001600160801b038091169116906f1a0111ea397fe69a4b1ba7b6434bacd75f5160206153185f395f51905f52602086013511158183141691101715610074576f1a0111ea397fe69a4b1ba7b6434bacd75f5160206153185f395f51905f52606085013511158183141691101715610074576080809282370190565b801561501a575061a1c090616cc05191616ce0925b6170608410614ff55783515f5160206152d85f395f51905f5291098015614fee57602082526020808301526020604083015260608201527f73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffffff60808201525f5160206152d85f395f51905f5260a082015260208160c08160055afa60203d141692815191601f1901905b80616ce010614fc65750505f5160206152d85f395f51905f5280616ce051830991616cc051900990616cc052616ce052565b5f5160206152d85f395f51905f5280835185099382519009928152601f199182019101614f94565b505f925050565b60205f5160206152d85f395f51905f5281928694965190099283865201930190614f0b565b90565b801561501a57506080616f806103005e6101006128e06103805e6080616f006104805e6101006129e06105005e60206103008080600f62029810fa60203d141661030051161561007457600190565b5f96945f969293945f1901925f5b86811061508a5750505050505050565b8483820483808260051b8801359215166150f7575b5087858406021c168682026101008110806150d6575b156150c5575b505060010161507a565b60ff19011b9099019860015f6150bb565b9a82821b019a61010089830111156150b5579b8282610100031c019b6150b5565b90038361509f565b600193925f926003820160021c845b81811061520257505084833510156001166151bb575b9360049184958261513696029461506c565b8192919381936f1a0111ea397fe69a4b1ba7b6434bacd781145f5160206153185f395f51905f5284148116806151b0575b156151a0575b6f1a0111ea397fe69a4b1ba7b6434bacd7905f5160206153185f395f51905f52600160801b891095111516911017161693565b600186019586109096019561516d565b5f9750879650615167565b936151369350815f5160206153185f395f51905f526f1a0111ea397fe69a4b1ba7b6434bacd76151f18460048181988c8b61506c565b929092149114169450915093615124565b828160021b8503600490818110615240575b50026101008110615229575b5060010161510e565b9760018092991b8960051b87013510169790615220565b90505f615214565b9290915f926001946003830160021c5f5b818110615271575050925f926004926151369561506c565b838160021b86036004908181106152af575b50026101008110615298575b50600101615259565b9760018092991b8960051b8501351016979061528f565b90505f61528356fe4e5280109d8f96b8bfb543a6b1af25fb56a9db616af85a90eedc558e3eb1ea2973eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff000000014997c5aa3a5fa07bcaf880a9054bef831effbd9cd58e46d9bb4fb88ef99de0db64774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff000000004382d0938a760120dd6cef8f3b90a0c38abae475e3d21e39365472b76d780272 +0x6108e06040526004361015610012575f80fd5b5f3560e01c80631e8e1e1314610078576342b7259714610030575f80fd5b34610074575f366003190112610074576040517f0000000000000000000000002fe3db07cc00f6dff002c37d5a17ebfad7aa88d06001600160a01b03168152602090f35b5f80fd5b346100745760403660031901126100745760043567ffffffffffffffff8111610074573660238201121561007457806004013567ffffffffffffffff81116100745760243691830101116100745760243567ffffffffffffffff8111610074573660238201121561007457806004013567ffffffffffffffff8111610074576024369160051b8301011161007457611ec060409114911416156100745760017f0000000000000000000000002fe3db07cc00f6dff002c37d5a17ebfad7aa88d0803b61428114813f7f24430c63ec2017f12ec4004d66c66314f418dd77fe38dd4c2475c8f2b259e0091416156100745781612700614280923c6121443614611ec435601314604435611e6014831616168015610074575f90731a0111ea397fe69a4b1ba7b6434bacd764774b846120a435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa612084351416731a0111ea397fe69a4b1ba7b6434bacd764774b8461206435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa6120443514161680614c43575b15614baf575b166080616bc061a1c05e808261a24052614b94575b5f90731a0111ea397fe69a4b1ba7b6434bacd764774b8461212435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa612104351416731a0111ea397fe69a4b1ba7b6434bacd764774b846120e435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa6120c43514161680614b77575b15614ae3575b1680916080616c4061a1c05e61a24052614ac8575b80156100745760806127005190525f60a0525f60c0525f60e0525f61010052601361012052610140611ee45b6121448110614aa457506064906191c0905b826107e48110614a87575f5160206151df5f395f51905f528592607f190160802080608052066169805260a090619940916101008201925b838310614a695784835f5160206151df5f395f51905f5284607f190160802080608052066169a0525f5160206151df5f395f51905f52602060802080608052066169c05260a0619a4061030083015b808410614a47575050619d40608083015b808410614a25575061010060806103b8858095614d69565b948181619e4037019201905b818310614a035750505f5160206151df5f395f51905f5260806103e8838095614d69565b928181619ec0370191607f190160802080608052066169e05260a0610100619f409301925b8383106149e55784835f5160206151df5f395f51905f5284607f19016080208060805206616a005260a090619fc0916102008201925b8383106149c75784835f5160206151df5f395f51905f5284607f19016080208060805206616a205260a090618500610cc08201905b8183106149965750505f5160206151df5f395f51905f528192607f19016080208060805206616a40525f5160206151df5f395f51905f5260206080208060805206616a605261012060806104cb83614cd3565b928181616ac0370191607f190160802092836080526001600160801b035f5160206151df5f395f51905f5260a0950616616a8052826182a052015b80821061496e57506080905f5160206151df5f395f51905f52611ec493607f1901832080845206616aa05261053a81614cd3565b508181616b4037010361007457801561007457616a205180915f5b6014811061495357506127805192616cc0846127c0515b617060831061492e57505050506105a55f5160206151df5f395f51905f525f51602061523f5f395f51905f528408918261706052614dfd565b925f5160206151df5f395f51905f52616cc092612760519009916127c0515b6170608210614909578585616ce05190616d00915b616e0083106148ec575f92611ee4905b61214482106148c757505061706051616cc05190616e005193616cc052616ce052616d0052616d2052616d4052616d6052801561007457616a0051610300525f61a300525f5b61014081106148b857506001805b6031821061488f5782618b4051618a40516185205190618a6051916185405161088052618a8051936185605194618aa0516108c0526185805190618ac0516185a0516108a052618ae05191886185c0519586946108805189618b0051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f529109955f5160206151df5f395f51905f529109936108a0515f5160206151df5f395f51905f52910992866108c051905f5160206151df5f395f51905f529109925f5160206151df5f395f51905f52910990610880515f5160206151df5f395f51905f52908c09905f5160206151df5f395f51905f528a8c095f5160206151df5f395f51905f529108905f5160206151df5f395f51905f529108905f5160206151df5f395f51905f529108905f5160206151df5f395f51905f529108905f5160206151df5f395f51905f529108905f5160206151df5f395f51905f529108905f5160206151df5f395f51905f5291088684618b2051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f5291085f5160206151df5f395f51905f5290600109956103005161a30051905f5160206151df5f395f51905f5291099661a1c051905f5160206151df5f395f51905f52910861a1c0526108a0515f5160206151df5f395f51905f52035f5160206151df5f395f51905f52905f08915f5160206151df5f395f51905f52035f5160206151df5f395f51905f52905f089061088051905f5160206151df5f395f51905f529108905f5160206151df5f395f51905f529108905f5160206151df5f395f51905f5291085f5160206151df5f395f51905f529060010961a1e051905f5160206151df5f395f51905f52910861a1e0525f5160206151df5f395f51905f52035f5160206151df5f395f51905f52905f08915f5160206151df5f395f51905f529108905f5160206151df5f395f51905f5291085f5160206151df5f395f51905f529060010961a20051905f5160206151df5f395f51905f529108906185e0515f5160206151df5f395f51905f52035f5160206151df5f395f51905f52905f089061088051905f5160206151df5f395f51905f529108905f5160206151df5f395f51905f5291085f5160206151df5f395f51905f5290600109918261a9605261030051906103005190610300515f5160206151df5f395f51905f529109905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f52910961a3005261a360515f5160206151df5f395f51905f529109905f5160206151df5f395f51905f52910861a20052614120905f9061a960825b84906152ef861015611ece57600186515f1a9601959182600514611ea2575081600614611e825781600814611e675781600d14611e3d5781601014611e195781601114611df55781602114611b5657508060191461180b5780601f146112b55780601b14610af657600b14610a85575f80fd5b600384519401935f905f5160206151df5f395f51905f526103005161a300510961a300525f5160206151df5f395f51905f5285611fe08360f31c1661a1c0019283519061ffff8160e81c16610ade575b50089052610a12565b90621fffe0849260e31c1661a3400151900989610ad5565b505082516002909301925f925061a96090839060f01c8015610e225780600114610cf65780600214610c1157600314610b2d575f80fd5b5f5160206151df5f395f51905f528080808080618b0051816185e05181035f0890088180618520518161858051918009097f404d21073985d14e432a4ad76d3fae06ca74314b950fe7b1d7f501cd31a8b374099008818061854051816185a051918009097f0b2cc8704264c6bd81bc620e9e524d4b73e9b2317679422ff7fa1603955649f10990088180618560518161862051918009097f0fdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f099008600109808452816103005161a300510961a300528161a2e05161a3605190090861a2e052610a12565b505f5160206151df5f395f51905f528080808080618ae051816185c05181035f0890088180618520518161858051918009097f1b8114c381b922fd5d6d241210e2d8a68ad5744053ba9e776118de4107b51ace099008818061854051816185a051918009097f3df32e4cc4cb2ed20e5d21899cf5331775990ccaec4c09b4e3717213fcc0d7630990088180618560518161862051918009097f3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc1099008600109808452816103005161a300510961a300528161a2e05161a3605190090861a2e052610a12565b505f5160206151df5f395f51905f528080807f73eda753299d7d483339d80809a1d7f7b67900f7fe6bfad98998021b7bb5732b81807f73eda753299d7d483339d80809a1d80553bca402fffe5bfeffffffff0000000181808080600160701b61852051088180600160701b6185405108600160381b0990088180600160701b6185605108600160701b099008818080618660518161868051600160381b0990086186a0518290600160701b09900881035f08900808818070241eabfffeb153ffffb9feffffffffaaab6187c0510981035f089008088180600160861b817f73eda753299d7d483339d80809a1d80553bda402fffe5b6e855000003ab000026187e051080981035f089008600109808452816103005161a300510961a3005261a240510861a24052610a12565b5061852051610400526185c051610520526185e051610480526186005161044052618540516104a0526187a0516104c0525f5160206151df5f395f51905f528080807f73eda753299d7d483339d80809a1d7d340dd972492de594de627fffefcb803498180618560516187805161054052618580516105805261876051610460526185a0516104205261874051610500526186205161056052618640516104e05281808080618660518161868051600160381b0990086186a0518290600160701b09900881035f089181808061044051600160701b09818061048051600160381b096105205108089181808083600160701b0981806104a051600160381b09610400510808918180806104c0516104e05109700c8557e86f90d0d89eed6eb5349a0f88200991818080610540516104e05109702cb9b546d20373eaf85e8f53db883cb5480991818080610460516104e0510970013af65741744bd7bb2c6872df2b8003200991818080610500516104e0510970340f2ebe380a0f5eff4360543988a61dc20991818080610440516104e0510970297784894e27525bc342b7fde37dba93660991818080610480516104e05109703212e00cde6d2002b119d800000347fcb809918180806104c0516105605109702cb9b546d20373eaf85e8f53db883cb548099181808061054051610560510970013af65741744bd7bb2c6872df2b800320099181808061046051610560510970340f2ebe380a0f5eff4360543988a61dc2099181808061050051610560510970297784894e27525bc342b7fde37dba93660991818080610440516105605109703212e00cde6d2002b119d800000347fcb809918180806104c051610420510970013af65741744bd7bb2c6872df2b800320099181808061054051610420510970340f2ebe380a0f5eff4360543988a61dc2099181808061046051610420510970297784894e27525bc342b7fde37dba93660991818080610500516104205109703212e00cde6d2002b119d800000347fcb809918180806104c051610580510970340f2ebe380a0f5eff4360543988a61dc2099181808061054051610580510970297784894e27525bc342b7fde37dba93660991818080610460516105805109703212e00cde6d2002b119d800000347fcb809918180806104c051840970297784894e27525bc342b7fde37dba936609918180808080610540518609703212e00cde6d2002b119d800000347fcb80993610520519009600160701b098180806104c0516104a05109703212e00cde6d2002b119d800000347fcb809818080610480516104a05109600160701b09818080610520516104a05109600160381b09818080610440516104005109600160701b09818080610480516104005109600160381b09816105205161040051090808080808080808080808080808080808080808080808080808080808818070241eabfffeb153ffffb9feffffffffaaab6187c0510981035f089008088180600160861b817f73eda753299d7d483339d80809a1d80553b9202d7ffe85d4800008bb200000016187e051080981035f089008600109808452816103005161a300510961a3005261a220510861a22052610a12565b505090505f905f61a96090616d40516103a052616d005161032052616d20516103e0526169a05161038052616980516103c0525f5160206151df5f395f51905f52806190e05181610320516103a0510809816103005161a30051090861a300525f5160206151df5f395f51905f52618b6051816103c05191816103c0515f0908095f5b600481106117de57505060015f5b600481106117bc5750600161a9e05260015b6004811061178a5750600161aac0526003805b61175857505f905f5b6004811061172b5750905f5160206151df5f395f51905f52808080808096816190c05197810391880908816103005161a3005109089381808080618be051948180618b8051816103c0515f090881618ba051916103c05190090895096190e0510881036191005108816190a0519361038051900890090881806103e05161032051088103600108099161030051900908610360526191605161034052618a00516102e05261852051610260526185405161028052618560516102c052618580516102a0526185a0516102405261862051610220526186405161020052618660516101e052618680516101c0526186a0516101a0526186c051610180526186e0516101605261870051610140526187205161012052618bc051610100526191405160e0525f5160206151df5f395f51905f5280618c005181035f0860010860c0525f5160206151df5f395f51905f52808060e05160010961034051088103619180510860a0525f5160206151df5f395f51905f5280806191205181806103805181806101005160c05109816103c05181806101205160c05109816103c05181806101405160c05109816103c05181806101605160c05109816103c05181806101805160c05109816103c05181806101a05160c05109816103c05181806101c05160c05109816103c05181806101e05160c05109816103c05181806102005160c05109816103c05181806102205160c05109816103c05181806102405160c05109816103c05181806102a05160c05109816103c05181806102c05160c05109816103c05181806102805160c05109816103c05181806102605160c05109816103c05181806102e05160c05109816103c0515f09080908090809080908090809080908090809080908090809080908090809080860a051090881806103e0516103205108810360010809816103005181805f51602061523f5f395f51905f528180610380518161010051816103c0518161012051816103c0518161014051816103c0518161016051816103c0518161018051816103c051816101a051816103c051816101c051816103c051816101e051816103c0518161020051816103c0518161022051816103c0518161024051816103c051816102a051816103c051816102c051816103c0518161028051816103c0518161026051816103c051816102e051816103c0515f09080908090809080908090809080908090809080908090809080908090809080860e0510908816103005181806103405181610320516103a051080981610300516103605109080908090861a30052610a12565b915f5160206151df5f395f51905f52600191818560051b8061aa6001519061a9e001510990089201611374565b5f5160206151df5f395f51905f528160051b808601519061aa600151095f19820160051b61aa6001525f19018061136b565b6001905f5160206151df5f395f51905f525f19820160051b808701519061a9e00151098160051b61a9e0015201611358565b905f5160206151df5f395f51905f526001918360051b86015190099101611346565b8060019160051b5f5160206151df5f395f51905f5261038051818361854001518708089086015201611338565b5050618a205161a9609081525f925082805b60058110611b3d57506185005161aa2052616d605161aa40525f5b60098110611b245750618a005161ab80525f5b60128110611b0b57505f5b60068110611af057505f5b60068110611ad557505f5b60058110611aba57505f5160206151df5f395f51905f5280808061ade0518103600108616d405109816103005161a30051090881808061ae80518181810391800908616d005109916103005190090861a3005260015b60068110611a7457505f5160206151df5f395f51905f52616a20516169a0510961b000525f5b600681106118f65750610a12565b600381026003810160128111611a6c575b8260051b908161aea001519161ade001519061b00051908194906169c051906169a0515b8a82851061199f57505050505050600193925f5160206151df5f395f51905f52808080957f4285088329c399ea457a8ca1d30f8957e74c7f529842a1579b4fee55b398292395820390088180616d2051616d0051088103890809816103005161a30051090861a300520961b00052016118e8565b9285979385969792939482809760051b809301519261aba001515f5160206151df5f395f51905f529087095f5160206151df5f395f51905f52908408905f5160206151df5f395f51905f5291085f5160206151df5f395f51905f529109985f5160206151df5f395f51905f529108905f5160206151df5f395f51905f5291085f5160206151df5f395f51905f529109947f08634d0aa021aaf843cab354fabb0062f6502437c6a09c006c083479590189d75f5160206151df5f395f51905f5291099360010192919061192b565b506012611907565b805f5160206151df5f395f51905f52808060019460051b61ade001515f19850160051b61af60015182039008616d405109816103005161a30051090861a30052016118c2565b80606060019202618ec001518160051b61af6001520161186c565b80606060019202618ea001518160051b61aea0015201611861565b80606060019202618e8001518160051b61ade0015201611856565b8060019160051b80618c4001519061aba001520161184b565b8060019160051b8061862001519061aa60015201611838565b8060019160051b8061852001519061a98001520161181d565b92939480915090600181515f1a91015f9260018316611de5575b505f9360028316611dcc575b815196875f1a8860011a908960021a9a60058b60041a960199611dbe575b505f5b818110611d715750505f5b818110611d125750505f5b878a821015611c4a5788519860118a60f01c9101995f5b60078110611bde5750505050600101611bb3565b8060051b8301515f6080525b600760805110611bfd5750600101611bca565b9a5f5160206151df5f395f51905f5290818d81600460805187018a0101515f1a60051b612ae001519160805160051b61ffff8960e01c16015190090990089a600160805101608052611bea565b5050959750955f905b8060031a8210611cd85750505f905b808210611c94575050600116611c7c575b50916001610a12565b905f5160206151df5f395f51905f5291510984611c73565b90935f5160206151df5f395f51905f526001918160058b519b019a81815f1a60051b612ae001519161ffff808260d81c16519160e81c165109099008940190611c62565b90945f5160206151df5f395f51905f526001918160038c519c019b61ffff8160e81c1651905f1a60051b612ae00151099008950190611c53565b6002895160f01c990198515f905b8a60078310611d3457505050600101611ba8565b600191929a5f5160206151df5f395f51905f5260038193519e019d8161ffff825f1a60051b612ae001519260e81c16518709099008990190611d20565b5f5b8a60078210611d86575050600101611b9d565b6001919a5f5160206151df5f395f51905f5260038193519e019d61ffff8160e81c1651905f1a60051b612ae001510990089901611d73565b84526020909301928b611b9a565b9350600181515f1a91019060051b612ae0015193611b7c565b905160f01c925060030187611b70565b935f5160206151df5f395f51905f5291506002865160f01c96019551900992610a12565b935f5160206151df5f395f51905f5291506002865160f01c96019551900892610a12565b935f5160206151df5f395f51905f529150600186515f1a96019560051b612ae00151900992610a12565b935f5160206151df5f395f51905f52809250035f0892610a12565b93915f5160206151df5f395f51905f529150601f19019182510892610a12565b92949150946003905160f01c920194611ec0575b5051916001610a12565b835260209092019184611eb6565b836169e051610840525f5160206151df5f395f51905f52618ae051815f51602061523f5f395f51905f526185c051099008610720526185205161082052618540516108005261856051610760525f5160206151df5f395f51905f526107605161076051096107e05261858051610860525f5160206151df5f395f51905f52610860516108605109610740526185a0516107a0525f5160206151df5f395f51905f526107a0516107a051096107805261862051610700525f5160206151df5f395f51905f526107005161070051096107c052618640516106e0525f5160206151df5f395f51905f526106e0516106e051096106c052618660516106a0525f5160206151df5f395f51905f526106a0516106a05109610680525f5160206151df5f395f51905f52618b0051815f51602061523f5f395f51905f526185e05109900861066052618b205161064052618b405161062052618a405161060052618a60516105e052618a80516105c0525f5160206151df5f395f51905f52618aa051815f51602061523f5f395f51905f52618600510990086105a0525f5160206151df5f395f51905f5280808080618c205181036001086191a0519009810381808080806106805161068051096106a051095f5160206151ff5f395f51905f5209818080806106c0516106c051096106e051095f51602061525f5f395f51905f5209818080806107c0516107c0510961070051095f5160206151bf5f395f51905f5209818080806107805161078051096107a051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e098180808061074051610740510961086051097ed94c46a8456352aa44d7a885ab59e3a36664e6fb25e826f8a4cd79822f053309818080806107e0516107e0510961076051097f1f61345b652161410c5e29f51e301ae56342af824bc110649393d2b911c50d3e098180610800517f40fa389feb2522bb934881ac9ed749aee2296502af592418c6b5675c0f560261098180610820517f70d8f2a733a64d650faccc9b1c2a766a9544bb3ff1a11ee73cb43947ef386633096105a0510808080808080808816108405181808080806106c0516106c051096106e051095f5160206151ff5f395f51905f5209818080806107c0516107c0510961070051095f51602061525f5f395f51905f5209818080806107805161078051096107a051095f5160206151bf5f395f51905f52098180808061074051610740510961086051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e09818080806107e0516107e0510961076051097ed94c46a8456352aa44d7a885ab59e3a36664e6fb25e826f8a4cd79822f0533098180610800517f27e7119226c42a6d19c1541904b99ae40685511ed2e078964b74594d38340849098180610820517f6d05a41959f539a7fc9ec0972ea1e3dbb6fc67dd51daf3414f7fbbb091c7274a0981805f51602061523f5f395f51905f526106a051096105c0510808080808080808816108405181808080806107c0516107c0510961070051095f5160206151ff5f395f51905f5209818080806107805161078051096107a051095f51602061525f5f395f51905f52098180808061074051610740510961086051095f5160206151bf5f395f51905f5209818080806107e0516107e0510961076051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e098180610800517f23a6684b942d726a22e4d5b8d8ff83aeaa773f62600184efe5d033d7c7c6e827098180610820517f2f5908b169c6cf1bd26dcf0f9e5105481f5164f3ece0582bf3098312167751a70981805f51602061523f5f395f51905f526106e051096105e05108080808080808816108405181808080806107805161078051096107a051095f5160206151ff5f395f51905f52098180808061074051610740510961086051095f51602061525f5f395f51905f5209818080806107e0516107e0510961076051095f5160206151bf5f395f51905f52098180610800517f24822e1af9aa2887c912c87eb0f20bd332330e7e55cd784de67cb407a9f05520098180610820517f726df1506749848155630b86ae25a82b281ecd050fe3a52d85a181fa87202e4b0981805f51602061523f5f395f51905f526107005109610600510808080808088161084051818080808061074051610740510961086051095f5160206151ff5f395f51905f5209818080806107e0516107e0510961076051095f51602061525f5f395f51905f52098180610800517f26c2cc87f95726b28f33ca03409a460ec987cfe12adae32769e3565865d07191098180610820517f222e83e70453dfee19b402e9fa8dfe2c4987b034d0be3ceb478b3022e97934c10981805f51602061523f5f395f51905f526107a05109610620510808080808816108405181808080806107e0516107e0510961076051095f5160206151ff5f395f51905f52098180610800517f6bd72f9cfc53af9d931896e77ea5c61244cb6d5fae8954f37dc7b9002f5aa78a098180610820517f5e1d3dbecda6214343e24a47f45c5d033197ad01b65a730af95dc57e90c491400981805f51602061523f5f395f51905f5261086051096106405108080808816108405181808080806106805161068051096106a051097f0fdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f09818080806106c0516106c051096106e051097f301cf56f9b4577112cc4241cddf6484aaadedbf1bbd0f2351adf2e41c2fb2ecd09818080806107c0516107c0510961070051097f5f3a15bab4ce4097b1edc3a25002694b92395ce355a8a12fe557459d9633f70109818080806107805161078051096107a051097f275a20361ea91992193920270d3e2d1f6361880ac0a439c64bef815d4469ba85098180808061074051610740510961086051097f31e823a45e567484c1544e310c0fa5cd66547a8f0dde659ac61698c30e838d2509818080806107e0516107e0510961076051097f26cc223e16f47c20e17cc6069605fa5a8af05ea4f6eb36029a641d23b818eb10098180610800517f4d0ea7f9c3fda06d9535b0fdafd8338bd47c2200b284fa71a325ff41ac358028098180610820517f5b1fc262a28cbb8bf75d9b1a6edaa74591ec24cd9a209512213cec3a3c0f1a5d09610660510808080808080808816108405181808080806106805161068051096106a051097f3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc109818080806106c0516106c051096106e051097f06ccb1c7d87f3c12a2bde4e68ac7f1e8b03481ba15d7f88f9a7f9b8310dd6d3409818080806107c0516107c0510961070051097f53fded36d490ba6b05a5d10fd99ffe5456baec6a6a8753199d5ebdc33c99790e09818080806107805161078051096107a051097f412c98232b6ab8a47aa76ee814ef7ec6261987c9802f2cfc490e007951a60ca5098180808061074051610740510961086051097f333f8046ece5579cbd6872449c57f2703dfc8864cfadc06d587ff104a0d0c1f209818080806107e0516107e0510961076051097f3509dd2fe3aac0080783557fec090fb1cb4b2b0901253c55282024331d1fe1a8098180610800517f52f789e4afc3801f7411102ee2f47cc5954a744e71cac98e75ea962a55a0a76f098180610820517f590ba402032e82eb1f660ef09796c5686345a5054ed96dae8e2d2336337887710961072051080808080808080881610840515f0908090809080908090809080908090808816103005161a3005109088161a9405161a1c0510961a1c0528161a9205161a1e0510961a1e0528161a8c05161a200510961a200528161a8605161a220510961a220528161a8005161a240510961a240528161a7a05161a260510961a260528161a7405161a280510961a280528161a6e05161a2a0510961a2a0528161a6805161a2c0510961a2c0528161a5c05161a2e0510961a2e05281035f08616d8052616a2051908160015f5b6014811061486c57505f5160206151df5f395f51905f5280808080888180988198616da0528103600108616dc05281808061278051816127a051809c81809c81809c818c819d9b829c9a839b6170405282096170605209806170205209090909090909090961700052616a4051600161738052600190617380905f915b602a83106148405784618a0061a3005261850061a320526190a061a340526190c061a3605261912061a3805261914061a3a0526191a061a3c052618a2061a3e052618a4061a40052618a6061a42052618a8061a44052618aa061a46052618ac061a48052618ae061a4a052618b0061a4c052618b2061a4e052618b4061a50052618b6061a52052618b8061a54052618ba061a56052618bc061a58052618be061a5a052618c0061a5c052618c2061a5e052618c4061a60052618c6061a62052618c8061a64052618ca061a66052618cc061a68052618ce061a6a052618d0061a6c052618d2061a6e052618d4061a70052618d6061a72052618d8061a74052618da061a76052618dc061a78052618de061a7a052618e0061a7c052618e2061a7e052618e4061a80052618e6061a82052616d8061a84052618a00516173a061a32060015b602b811061481557505050617ba0526186e0515f5160206151df5f395f51905f526189a0519181806173a051928184618700510990089381836189c05109900882806173c051958187618720510990089181866189e05109900890617bc052617be052818080619060518180619080519281886190e051099008956191005109900892818661916051099008936191805109900890617c0052617c205261852061a300526185c061a3205261884061a3405261854061a360526185e061a3805261886061a3a05261856061a3c05261860061a3e05261888061a4005261858061a4205261874061a440526188a061a460526185a061a4805261876061a4a0526188c061a4c05261862061a4e05261878061a500526188e061a5205261864061a540526187a061a5605261890061a5805261866061a5a0526187c061a5c05261892061a5e05261868061a600526187e061a6205261894061a640526186a061a6605261880061a6805261896061a6a0526186c061a6c05261882061a6e05261898061a70052618520516185c05161884051916173a061a36060015b600b81106147cc57505050617c4052617c6052617c8052618e8061a30052618ea061a32052618ec061a34052618ee061a36052618f0061a38052618f2061a3a052618f4061a3c052618f6061a3e052618f8061a40052618fa061a42052618fc061a44052618fe061a4605261900061a4805261902061a4a05261904061a4c052618e8051618ea051618ec051916173a061a36060015b6005811061478357505050617ca052617cc052617ce052616a6051616a80516182a0516170005191836170205181617040519581617060519188815f5160206151df5f395f51905f52035f5160206151df5f395f51905f529089085f5160206151df5f395f51905f528581038a085f5160206151df5f395f51905f528381038b08905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f529109915f5160206151df5f395f51905f5281810383085f5160206151df5f395f51905f5286810384085f5160206151df5f395f51905f5290600109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52908409895f5160206151df5f395f51905f5283810388085f5160206151df5f395f51905f5285810389085f5160206151df5f395f51905f5290600109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f5290830994878d5f5160206151df5f395f51905f5282810387085f5160206151df5f395f51905f5288810388085f5160206151df5f395f51905f5290600109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52908909612ff690614c60565b955f5160206151df5f395f51905f5283810382085f5160206151df5f395f51905f5289810383085f5160206151df5f395f51905f5290600109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52908809935f5160206151df5f395f51905f5282810385085f5160206151df5f395f51905f528a810386085f5160206151df5f395f51905f5290600109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f529086095f5160206151df5f395f51905f528381038b085f5160206151df5f395f51905f528681038c085f5160206151df5f395f51905f5290600109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52908209995f5160206151df5f395f51905f5286810389085f5160206151df5f395f51905f528281038a08905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52908c099a8b945f5160206151df5f395f51905f52035f5160206151df5f395f51905f52908a085f5160206151df5f395f51905f529109905f5160206151df5f395f51905f52035f5160206151df5f395f51905f529089085f5160206151df5f395f51905f529082099788965f5160206151df5f395f51905f52035f5160206151df5f395f51905f5291085f5160206151df5f395f51905f529109915f5160206151df5f395f51905f52910981617ca051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f5203935f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52916080013509905f5160206151df5f395f51905f529108925f5160206151df5f395f51905f52910990617cc051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52035f5160206151df5f395f51905f529108925f5160206151df5f395f51905f52910990617ce051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52035f5160206151df5f395f51905f5291085f5160206151df5f395f51905f52825f09905f5160206151df5f395f51905f52910887895f5160206151df5f395f51905f5285810388085f5160206151df5f395f51905f5282810389085f5160206151df5f395f51905f5290600109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f528881038b085f5160206151df5f395f51905f528781038c085f5160206151df5f395f51905f528481038d08905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291098a5f5160206151df5f395f51905f528a810385085f5160206151df5f395f51905f5289810386085f5160206151df5f395f51905f5290600109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f5290830991885f5160206151df5f395f51905f528c810382085f5160206151df5f395f51905f5287810383085f5160206151df5f395f51905f5290600109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52908509968c5f5160206151df5f395f51905f52878a096134bd90614c60565b965f5160206151df5f395f51905f52908809935f5160206151df5f395f51905f5282810385085f5160206151df5f395f51905f528a810386085f5160206151df5f395f51905f5290600109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f529086095f5160206151df5f395f51905f528381038b085f5160206151df5f395f51905f528681038c085f5160206151df5f395f51905f5290600109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52908209995f5160206151df5f395f51905f5286810389085f5160206151df5f395f51905f528281038a08905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52908c099a8b945f5160206151df5f395f51905f52035f5160206151df5f395f51905f52908a085f5160206151df5f395f51905f529109905f5160206151df5f395f51905f52035f5160206151df5f395f51905f529089085f5160206151df5f395f51905f529082099788965f5160206151df5f395f51905f52035f5160206151df5f395f51905f5291085f5160206151df5f395f51905f529109915f5160206151df5f395f51905f52910981617c4051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f5203935f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52916060013509905f5160206151df5f395f51905f529108925f5160206151df5f395f51905f52910990617c6051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52035f5160206151df5f395f51905f529108925f5160206151df5f395f51905f52910990617c8051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52035f5160206151df5f395f51905f529108915f5160206151df5f395f51905f529109905f5160206151df5f395f51905f529108905f5160206151df5f395f51905f5281810389085f5160206151df5f395f51905f52906001095f5160206151df5f395f51905f5282810388085f5160206151df5f395f51905f528a81038908905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5289810382085f5160206151df5f395f51905f52906001095f5160206151df5f395f51905f5290830961387090614c60565b5f5160206151df5f395f51905f528a810383085f5160206151df5f395f51905f52906001095f5160206151df5f395f51905f52908209915f5160206151df5f395f51905f528181038c085f5160206151df5f395f51905f52906001095f5160206151df5f395f51905f52908409905f5160206151df5f395f51905f528c81038b085f5160206151df5f395f51905f529083099384925f5160206151df5f395f51905f528381038d085f5160206151df5f395f51905f529109915f5160206151df5f395f51905f52035f5160206151df5f395f51905f52908c085f5160206151df5f395f51905f528e81038d08905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52910981617c0051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f5203915f5160206151df5f395f51905f5291095f5160206151df5f395f51905f529060408c013509905f5160206151df5f395f51905f529108925f5160206151df5f395f51905f52910990617c2051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52035f5160206151df5f395f51905f529108915f5160206151df5f395f51905f529109905f5160206151df5f395f51905f529108905f5160206151df5f395f51905f5281810387085f5160206151df5f395f51905f52906001095f5160206151df5f395f51905f5282810386085f5160206151df5f395f51905f528881038708905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52828209925f5160206151df5f395f51905f5289810382085f5160206151df5f395f51905f52906001095f5160206151df5f395f51905f52908509613b1190614c60565b915f5160206151df5f395f51905f528a810383085f5160206151df5f395f51905f52906001095f5160206151df5f395f51905f52908409935f5160206151df5f395f51905f52908509935f5160206151df5f395f51905f528b81038a085f5160206151df5f395f51905f529086099485935f5160206151df5f395f51905f52035f5160206151df5f395f51905f52908b085f5160206151df5f395f51905f529109915f5160206151df5f395f51905f52910981617bc051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f5203915f5160206151df5f395f51905f5291095f5160206151df5f395f51905f529060208a013509905f5160206151df5f395f51905f529108925f5160206151df5f395f51905f52910990617be051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52035f5160206151df5f395f51905f529108915f5160206151df5f395f51905f529109905f5160206151df5f395f51905f529108925f5160206151df5f395f51905f52035f5160206151df5f395f51905f529108613cd290614c60565b90617ba0515f5160206151df5f395f51905f52039035905f5160206151df5f395f51905f529108905f5160206151df5f395f51905f529109915f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291089081616e4052616aa05180808094616da051616dc051916182a051925f5160206151df5f395f51905f52856001096001600160801b038116955f5160206151df5f395f51905f5291096001600160801b038116965f5160206151df5f395f51905f5291096001600160801b038116995f5160206151df5f395f51905f5291096001600160801b038116975f5160206151df5f395f51905f5291096001600160801b03169260806198c061a3005e600161a38052608061994061a3a05e6173c05161a420526080619d4061a4405e6173e05161a4c05260806199c061a4e05e6174005161a560526080619dc061a5805e6174205161a600526080619f4061a6205e6174405161a6a052608061578061a6c05e6174605161a74052608061550061a7605e6174805161a7e052608061558061a8005e6174a05161a88052608061560061a8a05e6174c05161a92052608061568061a9405e6174e05161a9c052608061570061a9e05e6175005161aa6052608061530061aa805e6175205161ab0052608061538061ab205e6175405161aba052608061540061abc05e6175605161ac4052608061548061ac605e6175805161ace052608061580061ad005e6175a05161ad8052608061588061ada05e6175c05161ae2052608061590061ae405e6175e05161aec052608061598061aee05e6176005161af60526080615b8061af805e6176205161b000526080615f0061b0205e6176405161b0a052608061600061b0c05e6176605161b14052608061608061b1605e6176805161b1e052608061610061b2005e6176a05161b28052608061618061b2a05e6176c05161b32052608061620061b3405e6176e05161b3c052608061628061b3e05e6177005161b46052608061630061b4805e6177205161b50052608061638061b5205e6177405161b5a052608061640061b5c05e6177605161b64052608061648061b6605e6177805161b6e052608061650061b7005e6177a05161b78052608061658061b7a05e6177c05161b82052608061660061b8405e6177e05161b8c052608061668061b8e05e6178005161b96052608061670061b9805e6178205161ba0052608061678061ba205e6178405161baa052608061680061bac05e6178605161bb4052608061688061bb605e6178805161bbe052608061690061bc005e6178a05161bc80526178c051915f5160206151df5f395f51905f529083096080619fc061bca05e818161bd20525f5160206151df5f395f51905f529109608061a04061bd405e818161bdc0525f5160206151df5f395f51905f52910990608061a0c061bde05e8161be6052608061a14061be805e5f5160206151df5f395f51905f52910961bf00526080615a0061bf205e61a1c0515f5160206151df5f395f51905f5290820961bfa0526080615a8061bfc05e61a1e0515f5160206151df5f395f51905f5290820961c040526080615b0061c0605e61a200515f5160206151df5f395f51905f5290820961c0e0526080615c0061c1005e61a220515f5160206151df5f395f51905f5290820961c180526080615c8061c1a05e61a240515f5160206151df5f395f51905f5290820961c220526080615d0061c2405e61a260515f5160206151df5f395f51905f5290820961c2c0526080615d8061c2e05e61a280515f5160206151df5f395f51905f5290820961c360526080615e0061c3805e61a2a0515f5160206151df5f395f51905f5290820961c400526080615e8061c4205e61a2c0515f5160206151df5f395f51905f5290820961c4a0526080615f8061c4c05e61a2e0515f5160206151df5f395f51905f52910961c54052608061974061c5605e8361c5e05260806197c061c6005e836173a051905f5160206151df5f395f51905f52910961c68052608061984061c6a05e836173c051905f5160206151df5f395f51905f52910961c720526080619cc061c7405e8461c7c0526080619e4061c7e05e846173a051905f5160206151df5f395f51905f52910961c860526080619ec061c8805e846173c051905f5160206151df5f395f51905f52910961c9005260806191c061c9205e8761c9a052608061924061c9c05e876173a051905f5160206151df5f395f51905f52910961ca405260806192c061ca605e876173c051905f5160206151df5f395f51905f52910961cae052608061934061cb005e876173e051905f5160206151df5f395f51905f52910961cb805260806193c061cba05e8761740051905f5160206151df5f395f51905f52910961cc2052608061944061cc405e8761742051905f5160206151df5f395f51905f52910961ccc05260806194c061cce05e8761744051905f5160206151df5f395f51905f52910961cd6052608061954061cd805e8761746051905f5160206151df5f395f51905f52910961ce005260806195c061ce205e8761748051905f5160206151df5f395f51905f52910961cea052608061964061cec05e876174a051905f5160206151df5f395f51905f52910961cf405260806196c061cf605e876174c051905f5160206151df5f395f51905f52910961cfe0526080619a4061d0005e8561d080526080619ac061d0a05e856173a051905f5160206151df5f395f51905f52910961d120526080619b4061d1405e856173c051905f5160206151df5f395f51905f52910961d1c0526080619bc061d1e05e856173e051905f5160206151df5f395f51905f52910961d260526080619c4061d2805e8561740051905f5160206151df5f395f51905f52910961d300526080616ac061d3205e868261d3a05261475a575b5f5160206151df5f395f51905f5296979387809693948180808080809a8199099c60808601350999606085013509966040840135099360208301350990350808080808616e60526080616b40616f005e6080612860815e805f5160206151df5f395f51905f52616e605181035f0861010052614743575b806080616e806101005e61472b575b6080616b406101005e80616a805161018052614712575b806146fa575b608080616f805e7c70616972696e672d62617463682d6163632d6b7a670000000000000000610100526080616f806101205e6080616f006101a05e6080616c406102205e6080616bc06102a05e5f5160206151df5f395f51905f5261022061010020069081156146f1575b6080616c406101005e8082610180526146d8575b806080616f806101805e6146be575b80916080616bc06101005e610180526146a5575b806080616f006101805e61468b575b80156100745761467f90614f24565b50600160805260206080f35b506080616f0061010080600b61c350fa60803d1416614670565b50608061010060a081600c61f230fa60803d1416614661565b506080616f8061010080600b61c350fa60803d141661464d565b50608061010060a081600c61f230fa60803d141661463e565b6001915061462a565b5060808061010081600b61c350fa60803d14166145bf565b50608061010060a081600c61f230fa60803d14166145b9565b5060808061010081600b61c350fa60803d14166145a2565b5060808060a081600c61f230fa60803d1416614593565b9692955090926080616e806130c061a300600c6208c678fa3d608014169592969391909361451c565b909194606060205f5160206151df5f395f51905f526001938180808c9b9a9b519b818d8c515109900899818c878c01515109900899604089015151099008970193019101612e37565b909194606060205f5160206151df5f395f51905f526001938180808c9b9a9b519b818d8c515109900899818c878c01515109900899604089015151099008970193019101612da1565b9091926020805f5160206151df5f395f51905f52600193818851885151099008950193019101612c07565b5f5160206151df5f395f51905f52826020600193019509926001600160801b0384168552019192612ac4565b91905f5160206151df5f395f51905f528086818460019509099280099201612a47565b5f5160206151df5f395f51905f5260019161030051900991828160051b61a3400152019061063d565b5f61a1c082015260200161062f565b909360205f5160206151df5f395f51905f5281928188358651099008950191016105e9565b5f5160206151df5f395f51905f52602091845190089201916105d9565b5f5160206151df5f395f51905f528382828060209587510988098552099101906105c4565b602091815f5160206151df5f395f51905f52809381038708855209910190859061056c565b915f5160206151df5f395f51905f5281600192099201610555565b9181355f5160206151df5f395f51905f52811015610074578152602090810192910190610506565b90928235915f5160206151df5f395f51905f5283101561007457828152918152602090810193928101929101610478565b919060806149d6838293614d69565b93818482370191019190610443565b919060806149f4838293614d69565b9381848237019101919061040d565b9190926080614a13838293614d69565b938184823701910192909291926103c4565b916080614a36858293969496614d69565b9481848237019101919291926103a0565b916080614a58858293969496614d69565b94818482370191019192919261038f565b91906080614a78838293614d69565b93818482370191019190610340565b614a95608092918392614d69565b92818582370192019190610308565b90602080918335945f5160206151df5f395f51905f528610169481520191016102f6565b506080616c4060a061a1c0600c61f230fa60803d14166102ca565b9050614af9600160381b600760386120c4615006565b9392614b0f600160381b6007603861210461514f565b5093919092169580614b53575b15614b2b575b505050506102b5565b909192948383178287171715151694616c4052616c6052616c8052616ca05283808080614b22565b95838317828617171516955f616c40525f616c60525f616c80525f616ca052614b1c565b915082915f616c40525f616c60525f616c80525f616ca0526102af565b506080616bc060a061a1c0600c61f230fa60803d141661022c565b9050614bc5600160381b60076038612044615006565b9392614bdb600160381b6007603861208461514f565b5093919092169580614c1f575b15614bf7575b50505050610217565b909192948383178287171715151694616bc052616be052616c0052616c205283808080614bee565b95838317828617171516955f616bc0525f616be0525f616c00525f616c2052614be8565b915082915f616bc0525f616be0525f616c00525f616c2052610211565b801561007457602061260052602061262052602061264052612660527f73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffffff612680525f5160206151df5f395f51905f526126a052602061260060c08160055afa156100745760203d03610074576126005190565b80356040820135908060801c610074578160801c610074576001600160801b038091169116906f1a0111ea397fe69a4b1ba7b6434bacd75f51602061521f5f395f51905f52602085013511158183141691101715610074576f1a0111ea397fe69a4b1ba7b6434bacd75f51602061521f5f395f51905f526060840135111581831416911017156100745760809060a03761012090565b81356040830135908060801c610074578160801c610074576001600160801b038091169116906f1a0111ea397fe69a4b1ba7b6434bacd75f51602061521f5f395f51905f52602086013511158183141691101715610074576f1a0111ea397fe69a4b1ba7b6434bacd75f51602061521f5f395f51905f52606085013511158183141691101715610074576080809282370190565b8015614f21575061a1c090616cc05191616ce0925b6170608410614efc5783515f5160206151df5f395f51905f5291098015614ef557602082526020808301526020604083015260608201527f73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffffff60808201525f5160206151df5f395f51905f5260a082015260208160c08160055afa60203d141692815191601f1901905b80616ce010614ecd5750505f5160206151df5f395f51905f5280616ce051830991616cc051900990616cc052616ce052565b5f5160206151df5f395f51905f5280835185099382519009928152601f199182019101614e9b565b505f925050565b60205f5160206151df5f395f51905f5281928694965190099283865201930190614e12565b90565b8015614f2157506080616f806103005e6101006128e06103805e6080616f006104805e6101006129e06105005e60206103008080600f62029810fa60203d141661030051161561007457600190565b5f96945f969293945f1901925f5b868110614f915750505050505050565b8483820483808260051b880135921516614ffe575b5087858406021c16868202610100811080614fdd575b15614fcc575b5050600101614f81565b60ff19011b9099019860015f614fc2565b9a82821b019a6101008983011115614fbc579b8282610100031c019b614fbc565b900383614fa6565b600193925f926003820160021c845b81811061510957505084833510156001166150c2575b9360049184958261503d960294614f73565b8192919381936f1a0111ea397fe69a4b1ba7b6434bacd781145f51602061521f5f395f51905f5284148116806150b7575b156150a7575b6f1a0111ea397fe69a4b1ba7b6434bacd7905f51602061521f5f395f51905f52600160801b891095111516911017161693565b6001860195861090960195615074565b5f975087965061506e565b9361503d9350815f51602061521f5f395f51905f526f1a0111ea397fe69a4b1ba7b6434bacd76150f88460048181988c8b614f73565b92909214911416945091509361502b565b828160021b8503600490818110615147575b50026101008110615130575b50600101615015565b9760018092991b8960051b87013510169790615127565b90505f61511b565b9290915f926001946003830160021c5f5b818110615178575050925f9260049261503d95614f73565b838160021b86036004908181106151b6575b5002610100811061519f575b50600101615160565b9760018092991b8960051b85013510169790615196565b90505f61518a56fe4e5280109d8f96b8bfb543a6b1af25fb56a9db616af85a90eedc558e3eb1ea2973eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff000000014997c5aa3a5fa07bcaf880a9054bef831effbd9cd58e46d9bb4fb88ef99de0db64774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff000000004382d0938a760120dd6cef8f3b90a0c38abae475e3d21e39365472b76d780272 diff --git a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.sol b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.sol index 64ba62d74..295e1067f 100644 --- a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.sol +++ b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.sol @@ -270,7 +270,7 @@ contract Halo2Verifier { function verifyProof( bytes calldata proof, uint256[] calldata instances - ) external returns (bool) { + ) external view returns (bool) { // Cheap ABI-shape guard before any generated memory work: // - proof head must point at the bytes payload; // - instances head must point at the generated instance array. @@ -860,23 +860,9 @@ contract Halo2Verifier { // The caller checks `out` and reverts before transcript work if // any decode, canonicality, or precompile validation failed. } - - - // Section-boundary gas-attribution checkpoint. Emits a - // single LOG1 (no data) with topic = (id << 248) | gas(). - // Cost: 375 (LOG base) + 375 (1 topic) = 750 gas/call. - // Host-side parses the topic into (id, gas_left) and prints - // pairwise deltas (see `dump_gas_checkpoints`). - function gas_checkpoint(id) { - log1(0, 0, or(shl(248, id), gas())) - } - let r := FR_MODULUS let success := true - - gas_checkpoint(1) // entry: before VK loading - // =============================================================== // VK loading: either bake in the embedded VK bytes or fetch // them from the linked AUTHORIZED_VK contract. @@ -955,7 +941,6 @@ contract Halo2Verifier { // where the verifier converts failure to a revert. success := validate_public_accumulator(success, r) if iszero(success) { revert(0, 0) } - gas_checkpoint(2) // after VK loading + accumulator public-input precheck // =============================================================== // Transcript: VK digest + instances + proof. @@ -1025,7 +1010,6 @@ contract Halo2Verifier { buf_len := common_word(buf_len, inst_be) } } - gas_checkpoint(3) // after VK digest + committed_pi + instance absorbs // =============================================================== // Per-user-phase reads + challenge squeezes. @@ -1063,7 +1047,6 @@ contract Halo2Verifier { advice_walk := add(advice_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) } - gas_checkpoint(4) // after user-phase advice reads + user challenge squeezes // ---- theta ---- // From this point onward the transcript alternates between @@ -1083,7 +1066,6 @@ contract Halo2Verifier { lookup_m_walk := add(lookup_m_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) } - gas_checkpoint(5) // after theta squeeze + lookup multiplicities // ---- beta, gamma ---- // beta and gamma are the permutation/lookup randomizers. They are @@ -1103,7 +1085,6 @@ contract Halo2Verifier { perm_z_walk := add(perm_z_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) } - gas_checkpoint(6) // after beta/gamma + permutation Z products // ---- lookup helpers + accumulators (per-lookup) ---- // Each lookup contributes zero or more helper commitments followed // by its lookup accumulator Z commitment. The generated layout keeps @@ -1144,7 +1125,6 @@ contract Halo2Verifier { calldatacopy(lookup_z_walk, proof_cptr, 0x80) lookup_z_walk := add(lookup_z_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) - gas_checkpoint(7) // after lookup helpers + Z accumulators // ---- trash_challenge ---- // Midnight squeezes this challenge unconditionally, even when the @@ -1164,7 +1144,6 @@ contract Halo2Verifier { trashcan_walk := add(trashcan_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) } - gas_checkpoint(8) // after trash_challenge + trashcans // ---- y ---- // y batches all quotient identities. Quotient commitments are read @@ -1188,7 +1167,6 @@ contract Halo2Verifier { quotient_walk := add(quotient_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) } - gas_checkpoint(9) // after y squeeze + quotient-limb reads // ---- x ---- // x is the main evaluation point. Values read after this point are @@ -1297,7 +1275,6 @@ contract Halo2Verifier { // `success` carries deferred canonicality failures from public // instance reads. G1/proof scalar helpers revert immediately. if iszero(success) { revert(0, 0) } - gas_checkpoint(10) // after evaluations + x1/x2 + f_com + x3 + q_evals + x4 + pi (transcript done) // =============================================================== // Lagrange & instance-evaluation block (pure Fr arithmetic). @@ -1377,7 +1354,6 @@ contract Halo2Verifier { mstore(L_0_MPTR, l_0) mstore(INSTANCE_EVAL_MPTR, instance_eval) } - gas_checkpoint(11) // after Lagrange + instance evaluation block if iszero(success) { revert(0, 0) } @@ -2803,7 +2779,6 @@ contract Halo2Verifier { mstore(QUOTIENT_EVAL_MPTR, linearization_expected_eval) pop(y) } - gas_checkpoint(12) // after batched identity numerator reconstruction // =============================================================== // Prepare linearization scalars for the final PCS MSM. @@ -2845,7 +2820,6 @@ contract Halo2Verifier { mstore(QUOTIENT_MPTR, x_split) mstore(add(QUOTIENT_MPTR, 0x20), one_minus_x_n) } - gas_checkpoint(13) // after linearization scalar prep // =============================================================== // PCS computation (multi-prepare emitter from Step 5). @@ -2860,7 +2834,6 @@ contract Halo2Verifier { { // Generated PCS sub-block 1. These lines are // emitted by the multi-prepare lowering pass and are kept - // grouped so gas checkpoints can attribute their cost. { // 4 distinct rotation(s) let x := mload(X_MPTR) @@ -2884,10 +2857,8 @@ contract Halo2Verifier { x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) mstore(add(ROT_POINTS_MPTR, 0x0), x_pow_of_omega) } - gas_checkpoint(17) // after PCS sub-block 1 // Generated PCS sub-block 2. These lines are // emitted by the multi-prepare lowering pass and are kept - // grouped so gas checkpoints can attribute their cost. { // pre-compute 43 x1 power(s) let x1 := mload(X1_MPTR) @@ -2900,10 +2871,8 @@ contract Halo2Verifier { mstore(p, and(acc, 0xffffffffffffffffffffffffffffffff)) } } - gas_checkpoint(18) // after PCS sub-block 2 // Generated PCS sub-block 3. These lines are // emitted by the multi-prepare lowering pass and are kept - // grouped so gas checkpoints can attribute their cost. { // q_eval_set[0]: 43 commitment(s) (rolled, m>=4) // stage per-(commit, rotation) eval source addresses @@ -2961,10 +2930,8 @@ contract Halo2Verifier { } mstore(add(Q_EVAL_SET_MPTR, 0x0), q_eval_set_0) } - gas_checkpoint(19) // after PCS sub-block 3 // Generated PCS sub-block 4. These lines are // emitted by the multi-prepare lowering pass and are kept - // grouped so gas checkpoints can attribute their cost. { // q_eval_set[1]: 3 commitment(s) let q_eval_set_0 := mload(0x86e0) @@ -2976,10 +2943,8 @@ contract Halo2Verifier { mstore(add(Q_EVAL_SET_MPTR, 0x20), q_eval_set_0) mstore(add(Q_EVAL_SET_MPTR, 0x40), q_eval_set_1) } - gas_checkpoint(20) // after PCS sub-block 4 // Generated PCS sub-block 5. These lines are // emitted by the multi-prepare lowering pass and are kept - // grouped so gas checkpoints can attribute their cost. { // q_eval_set[2]: 3 commitment(s) let q_eval_set_0 := mload(0x9060) @@ -2991,10 +2956,8 @@ contract Halo2Verifier { mstore(add(Q_EVAL_SET_MPTR, 0x60), q_eval_set_0) mstore(add(Q_EVAL_SET_MPTR, 0x80), q_eval_set_1) } - gas_checkpoint(21) // after PCS sub-block 5 // Generated PCS sub-block 6. These lines are // emitted by the multi-prepare lowering pass and are kept - // grouped so gas checkpoints can attribute their cost. { // q_eval_set[3]: 11 commitment(s) (rolled, m>=4) // stage per-(commit, rotation) eval source addresses @@ -3048,10 +3011,8 @@ contract Halo2Verifier { mstore(add(Q_EVAL_SET_MPTR, 0xc0), q_eval_set_1) mstore(add(Q_EVAL_SET_MPTR, 0xe0), q_eval_set_2) } - gas_checkpoint(22) // after PCS sub-block 6 // Generated PCS sub-block 7. These lines are // emitted by the multi-prepare lowering pass and are kept - // grouped so gas checkpoints can attribute their cost. { // q_eval_set[4]: 5 commitment(s) (rolled, m>=4) // stage per-(commit, rotation) eval source addresses @@ -3087,10 +3048,8 @@ contract Halo2Verifier { mstore(add(Q_EVAL_SET_MPTR, 0x120), q_eval_set_1) mstore(add(Q_EVAL_SET_MPTR, 0x140), q_eval_set_2) } - gas_checkpoint(23) // after PCS sub-block 7 // Generated PCS sub-block 8. These lines are // emitted by the multi-prepare lowering pass and are kept - // grouped so gas checkpoints can attribute their cost. { // f_eval via Horner over 5 reversed set(s) let x2 := mload(X2_MPTR) @@ -3256,10 +3215,8 @@ contract Halo2Verifier { } mstore(F_EVAL_MPTR, f_eval) } - gas_checkpoint(24) // after PCS sub-block 8 // Generated PCS sub-block 9. These lines are // emitted by the multi-prepare lowering pass and are kept - // grouped so gas checkpoints can attribute their cost. { // build final_com and v (KZG single-opening proof, fused MSM) // final MSM input length from circuit/VK shape: 78 term(s) @@ -3451,10 +3408,8 @@ contract Halo2Verifier { } mstore(V_MPTR, v) } - gas_checkpoint(25) // after PCS sub-block 9 // Generated PCS sub-block 10. These lines are // emitted by the multi-prepare lowering pass and are kept - // grouped so gas checkpoints can attribute their cost. { // Scale z*pi - vG before the final pairing check // pairing inputs (LHS = pi; RHS = final_com - v*G + x3*pi) @@ -3483,7 +3438,6 @@ contract Halo2Verifier { mcopy(PAIRING_RHS_MPTR, 0x80, 0x80) } } - gas_checkpoint(14) // after PCS computation block (= sub-block 6) // Batch the prevalidated public IVC accumulator pairing equation // into the final KZG pairing. @@ -3542,7 +3496,6 @@ contract Halo2Verifier { success := and(success, eq(returndatasize(), 0x80)) } } - gas_checkpoint(15) // after public accumulator pairing batch prep (omitted for no-accumulator VKs) // The Yul `ec_pairing` helper checks // e(arg0, G2_BASE) * e(arg1, NEG_S_G2_BASE) == 1 @@ -3559,7 +3512,6 @@ contract Halo2Verifier { // pairing argument order. Pass them swapped to ec_pairing. if iszero(success) { revert(0, 0) } success := ec_pairing(success, PAIRING_RHS_MPTR, PAIRING_LHS_MPTR) - gas_checkpoint(16) // after final ec_pairing diff --git a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md index 25ba416ac..9b4c21b25 100644 --- a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md +++ b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md @@ -8,23 +8,23 @@ chain. | Contract | Address | | --- | --- | -| `Halo2VerifyingKey` | `0x2132E1C5f2aFE710E0F3fE01ec70E95000F99882` | -| `Halo2Verifier` | `0x3d7f4A7BF9EB60EC7909880A68eaf41e4fB4B3b5` | +| `Halo2VerifyingKey` | `0x2fE3dB07Cc00f6dFF002C37d5A17ebfAd7aA88D0` | +| `Halo2Verifier` | `0x049510AfEC69eD54409dabB57195FfD38400F4D0` | ## Transactions | Action | Transaction | | --- | --- | -| Deploy VK | `0x5283e8cfd54be98b752cfee8fda8c0f02b196f8b0053af33850e4b2cd20df80a` | -| Deploy verifier | `0x69c4f9d0ac9098357346b3986c3fd48d062738124df91a73a89e314ecf044e0a` | -| Verify fresh proof | `0x91bac90773f107016103be132b543af5ea4457527e3866484e62cbb6e01fdb97` | +| Deploy VK | `0xd6e7c6180a57c424327df03fe5b4ed0a274c73a4e8f681913582db5cf9e809cf` | +| Deploy verifier | `0x2236dc333064a88e7f23fc669570f72bf52163ca1cf637c29a6e77b743bfa60e` | +| Verify fresh proof | `0xd536e9162bab92c71b7b9f1bb94f00def8b50e441144c6aebd7708a51400abfb` | ## Runtime Metadata | Contract | Runtime bytes | Runtime code hash | | --- | ---: | --- | | `Halo2VerifyingKey` | `17025` | `0x24430c63ec2017f12ec4004d66c66314f418dd77fe38dd4c2475c8f2b259e009` | -| `Halo2Verifier` | `21368` | `0x21eafc97654cfecd897cd8957217254ce0570813e59a36bc835e8bc459ee9b55` | +| `Halo2Verifier` | `21119` | `0x1422a1464cc070cd2d88cc9468edf6d88d99ead6cacbd942bfb0257e209f3b10` | ## Files diff --git a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/deployment.json b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/deployment.json index 7f0b5cbbd..b9145d3b1 100644 --- a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/deployment.json +++ b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/deployment.json @@ -4,8 +4,8 @@ "deployer": "0x609A4Ff8347Bb82d65a12A597298F256Fd4DE493", "contracts": { "Halo2VerifyingKey": { - "address": "0x2132E1C5f2aFE710E0F3fE01ec70E95000F99882", - "deploymentTx": "0x5283e8cfd54be98b752cfee8fda8c0f02b196f8b0053af33850e4b2cd20df80a", + "address": "0x2fE3dB07Cc00f6dFF002C37d5A17ebfAd7aA88D0", + "deploymentTx": "0xd6e7c6180a57c424327df03fe5b4ed0a274c73a4e8f681913582db5cf9e809cf", "gasUsed": 3723458, "runtimeBytes": 17025, "runtimeCodeHash": "0x24430c63ec2017f12ec4004d66c66314f418dd77fe38dd4c2475c8f2b259e009", @@ -13,18 +13,18 @@ "sourceFile": "Halo2VerifyingKey.sol" }, "Halo2Verifier": { - "address": "0x3d7f4A7BF9EB60EC7909880A68eaf41e4fB4B3b5", - "deploymentTx": "0x69c4f9d0ac9098357346b3986c3fd48d062738124df91a73a89e314ecf044e0a", - "gasUsed": 4763839, - "runtimeBytes": 21368, - "runtimeCodeHash": "0x21eafc97654cfecd897cd8957217254ce0570813e59a36bc835e8bc459ee9b55", + "address": "0x049510AfEC69eD54409dabB57195FfD38400F4D0", + "deploymentTx": "0x2236dc333064a88e7f23fc669570f72bf52163ca1cf637c29a6e77b743bfa60e", + "gasUsed": 4709970, + "runtimeBytes": 21119, + "runtimeCodeHash": "0x1422a1464cc070cd2d88cc9468edf6d88d99ead6cacbd942bfb0257e209f3b10", "runtimeBytecodeFile": "Halo2Verifier.runtime.bytecode.hex", "sourceFile": "Halo2Verifier.sol" } }, "proofVerificationTx": { - "transactionHash": "0x91bac90773f107016103be132b543af5ea4457527e3866484e62cbb6e01fdb97", - "gasUsed": 1310889, + "transactionHash": "0xd536e9162bab92c71b7b9f1bb94f00def8b50e441144c6aebd7708a51400abfb", + "gasUsed": 1291664, "status": 1 }, "compiler": { From b53e3ebe49fcb1e3f41e6da66f73db609bc7c756 Mon Sep 17 00:00:00 2001 From: Julien Coolen Date: Tue, 19 May 2026 10:34:44 +0100 Subject: [PATCH 10/19] Add MCOPY constructor smoke test --- .../docs/plans/TESTING_STRATEGY.md | 10 ++++++---- .../reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md | 1 + proofs/solidity-verifier/src/lowering/tests.rs | 12 ++++++++---- .../templates/contracts/Halo2Verifier.sol | 2 +- .../partials/verifier/Constructors.sol | 18 ++++++++++-------- .../partials/verifier/PrecompileSmoke.sol | 17 +++++++++++++---- 6 files changed, 39 insertions(+), 21 deletions(-) diff --git a/proofs/solidity-verifier/docs/plans/TESTING_STRATEGY.md b/proofs/solidity-verifier/docs/plans/TESTING_STRATEGY.md index b92a2215f..8277b9f12 100644 --- a/proofs/solidity-verifier/docs/plans/TESTING_STRATEGY.md +++ b/proofs/solidity-verifier/docs/plans/TESTING_STRATEGY.md @@ -499,7 +499,7 @@ checkpoint. | Empty/edge circuit shapes | Shape fuzz circuits with no advice in a phase, no lookups, one lookup, additive selectors, complex selectors, next rotations, second phase advice, permutation on/off, and wide advice counts that stress memory layout. | | PCS/KZG/quotient | Mutate every quotient commitment, proof eval, opening proof, batching scalar source, quotient evaluator output, and external quotient return length. Assert the final pairing result is semantically checked, not just precompile call success. | | Accumulator-specific | Check accumulator schema consumes exactly the expected public input words. Test unused high limb bits, malformed identity encoding, x/y limb swaps, scalar mutation, zero/identity accumulator cases, and any future fixed-base tail. | -| Precompile/fail behavior | Constructor smoke tests for EIP-2537 are good; add tests for short return data, false pairing result, reverted precompile call, and stale return memory using generated-template mutations or a helper harness. | +| Precompile/fail behavior | Constructor smoke tests cover MCOPY and EIP-2537 identity calls; add tests for short return data, false pairing result, reverted precompile call, and stale return memory using generated-template mutations or a helper harness. | | Memory/layout | Fast generator tests should assert no overlap between VK, challenge, transcript, quotient, PCS, accumulator, and scratch regions. Keep these as compile-time/layout tests in `src/codegen/mod.rs` and `src/codegen/template.rs`. | | Production artifact checks | `verifyProof` production renders stay `external view`, no `LOG1`, no gas checkpoints, Solidity pragma `^0.8.24`, Cancun/Prague target, runtime size below EIP-170 with margin. | | Wrapper/application binding | Add small mock wrapper contracts that bind expected state root, program ID, chain/domain, caller/action hash, nullifier/nonce. Same proof with wrong wrapper context must reject. | @@ -586,7 +586,7 @@ Existing coverage already includes a healthy baseline: - Separate VK pinning and VK payload mutation. - Pinned quotient dependency checks. - Malformed calldata rejection. -- EIP-2537 constructor smoke tests. +- MCOPY/EIP-2537 constructor smoke tests. - Production render checks for `external view` and no gas logs. - Native/Solidity trace equivalence. - Scalar canonicality tests for proof scalars. @@ -617,9 +617,11 @@ A verifier profile is considered covered when: forms that the hand-rolled parser is not intended to accept. 5. EIP-2537 integration tests cover call failure, short return, semantic false return, and valid precompile behavior. -6. Production artifacts compile with the pinned compiler/EVM target and stay +6. Deployment CI exercises the exact destination chain or fork configuration, + including constructor-time MCOPY/EIP-2537 smoke tests. +7. Production artifacts compile with the pinned compiler/EVM target and stay within size limits. -7. Raw verifier NatSpec documents that application contracts must bind +8. Raw verifier NatSpec documents that application contracts must bind protocol semantics, and wrapper tests prove those bindings reject replay or wrong-context proofs. diff --git a/proofs/solidity-verifier/docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md b/proofs/solidity-verifier/docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md index 634482f70..39e363102 100644 --- a/proofs/solidity-verifier/docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md +++ b/proofs/solidity-verifier/docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md @@ -1549,6 +1549,7 @@ rules. The generated verifier constructor runs smoke tests: +- `MCOPY` one-word round trip in constructor scratch. - `G1ADD(identity, identity) -> identity`. - `G1MSM([(identity, 0)]) -> identity`. - `PAIRING_CHECK([(identity_g1, identity_g2)]) -> true`. diff --git a/proofs/solidity-verifier/src/lowering/tests.rs b/proofs/solidity-verifier/src/lowering/tests.rs index 10f2fcdef..e1950b2b8 100644 --- a/proofs/solidity-verifier/src/lowering/tests.rs +++ b/proofs/solidity-verifier/src/lowering/tests.rs @@ -1004,14 +1004,18 @@ fn failed_success_paths_do_not_enter_ec_precompiles() { } #[test] -fn verifier_constructor_smoke_tests_eip2537_precompiles() { +fn verifier_constructor_smoke_tests_runtime_prerequisites() { let verifier_template = verifier_template_corpus(); assert!( verifier_template.contains("function require_eip2537_precompiles() private view"), - "generated verifier should include a deployment-time EIP-2537 smoke test" + "generated verifier should include a deployment-time runtime prerequisite smoke test" ); for required in [ + "Smoke-check the Cancun/EIP-2537 runtime features", + "mcopy(add(scratch, {{ template_constants.word_bytes|hex() }}), scratch, {{ template_constants.word_bytes|hex() }})", + "eq(mload(add(scratch, {{ template_constants.word_bytes|hex() }})), 0x1234)", + "non-Cancun fork fails during deployment", "G1ADD(identity, identity) -> identity", "G1MSM([(identity, 0)]) -> identity", "PAIRING_CHECK([(identity_g1, identity_g2)]) -> true", @@ -1026,13 +1030,13 @@ fn verifier_constructor_smoke_tests_eip2537_precompiles() { ] { assert!( verifier_template.contains(required), - "constructor precompile smoke test missing expected check: {required}" + "constructor runtime-prerequisite smoke test missing expected check: {required}" ); } assert_eq!( verifier_template.matches("require_eip2537_precompiles();").count(), 4, - "every generated constructor shape should run the precompile smoke test" + "every generated constructor shape should run the runtime-prerequisite smoke test" ); assert!( verifier_template.contains("support MCOPY and EIP-2537"), diff --git a/proofs/solidity-verifier/templates/contracts/Halo2Verifier.sol b/proofs/solidity-verifier/templates/contracts/Halo2Verifier.sol index eea4edc23..c715ece09 100644 --- a/proofs/solidity-verifier/templates/contracts/Halo2Verifier.sol +++ b/proofs/solidity-verifier/templates/contracts/Halo2Verifier.sol @@ -30,7 +30,7 @@ pragma solidity ^0.8.24; /// Keccak digest, resets the transcript buffer to that digest, then samples /// by interpreting the digest as a big-endian integer modulo r. /// - Scalar inversion uses modexp(scalar, r-2, r). -/// - Constructors run a deployment-time smoke test for the EIP-2537 +/// - Constructors run deployment-time smoke tests for MCOPY and the EIP-2537 /// precompiles using identity inputs. Compile with Solidity >=0.8.24 and /// deploy only on chains/forks that support MCOPY and EIP-2537. contract Halo2Verifier { diff --git a/proofs/solidity-verifier/templates/partials/verifier/Constructors.sol b/proofs/solidity-verifier/templates/partials/verifier/Constructors.sol index e4de1710f..c13c57387 100644 --- a/proofs/solidity-verifier/templates/partials/verifier/Constructors.sol +++ b/proofs/solidity-verifier/templates/partials/verifier/Constructors.sol @@ -3,12 +3,12 @@ {%- match quotient_external %} {%- when Some with (_) %} /// @notice Create a verifier pinned to a verifying key and quotient evaluator. - /// @dev Checks EIP-2537 availability and verifies both dependency runtimes before storing their addresses. + /// @dev Checks MCOPY/EIP-2537 availability and verifies both dependency runtimes before storing their addresses. /// @param authorizedVk Address of the generated `Halo2VerifyingKey` runtime. /// @param authorizedQuotient Address of the generated `Halo2QuotientEvaluator` runtime. constructor(address authorizedVk, address authorizedQuotient) { - // Verifier correctness depends on chain support for the BLS12-381 - // precompiles; fail deployment before pinning any dependency address. + // Verifier correctness depends on chain support for MCOPY and the + // BLS12-381 precompiles; fail deployment before pinning dependencies. require_eip2537_precompiles(); // Pin the generated VK runtime exactly. The verifier later repeats the // codehash/length check before copying the VK payload for a proof. @@ -39,11 +39,12 @@ } {%- when None %} /// @notice Create a verifier pinned to a generated verifying key. - /// @dev Checks EIP-2537 availability and verifies the VK runtime before storing its address. + /// @dev Checks MCOPY/EIP-2537 availability and verifies the VK runtime before storing its address. /// @param authorizedVk Address of the generated `Halo2VerifyingKey` runtime. constructor(address authorizedVk) { // Embedded quotient path: only the external VK runtime needs to be - // pinned, but the curve precompiles are still mandatory. + // pinned, but the runtime opcode/precompile prerequisites are still + // mandatory. require_eip2537_precompiles(); require( authorizedVk.code.length == EXPECTED_VK_LENGTH @@ -61,7 +62,8 @@ /// @param authorizedQuotient Address of the generated `Halo2QuotientEvaluator` runtime. constructor(address authorizedQuotient) { // Embedded VK path: the verifier bytecode carries the VK payload, but - // the external quotient evaluator remains a pinned dependency. + // the runtime prerequisites and external quotient evaluator still + // need to be checked before storing the dependency address. require_eip2537_precompiles(); {%- match self.expected_quotient_codehash %} {%- when Some with (_) %} @@ -80,10 +82,10 @@ } {%- when None %} /// @notice Create a verifier with embedded verifier data. - /// @dev Checks EIP-2537 availability at deployment. + /// @dev Checks MCOPY/EIP-2537 availability at deployment. constructor() { // Fully embedded verifier: no external generated dependency exists, - // so deployment only checks precompile availability. + // so deployment only checks runtime opcode/precompile availability. require_eip2537_precompiles(); } {%- endmatch %} diff --git a/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol b/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol index ea824e2e9..860b623e2 100644 --- a/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol +++ b/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol @@ -1,10 +1,19 @@ - /// @notice Smoke-check the BLS12-381 precompiles required by the verifier. - /// @dev Uses identity inputs to catch absent EIP-2537 implementations, short return data, and incompatible pairing semantics at deployment. + /// @notice Smoke-check the Cancun/EIP-2537 runtime features required by the verifier. + /// @dev Exercises MCOPY and identity EIP-2537 inputs to catch incompatible chain/fork configurations at deployment. function require_eip2537_precompiles() private view { assembly ("memory-safe") { - // Scratch is reused for every precompile probe. Start with the - // EIP-2537 identity encoding for G1/G2: all-zero padded words. + // Scratch is reused for every runtime-prerequisite probe. let scratch := {{ memory.constructor_smoke_scratch_mptr|hex() }} + + // MCOPY must be available because the verifier uses it for + // proof-time point/scratch staging. Execute the opcode here so a + // non-Cancun fork fails during deployment instead of later proofs. + mstore(scratch, 0x1234) + mcopy(add(scratch, {{ template_constants.word_bytes|hex() }}), scratch, {{ template_constants.word_bytes|hex() }}) + if iszero(eq(mload(add(scratch, {{ template_constants.word_bytes|hex() }})), 0x1234)) { revert(0, 0) } + + // Start the EIP-2537 probes with the identity encoding for G1/G2: + // all-zero padded words. for { let off := 0 } lt(off, {{ template_constants.eip2537.smoke_scratch_bytes|hex() }}) { off := add(off, {{ template_constants.word_bytes|hex() }}) } { mstore(add(scratch, off), 0) } From 2b2bf49409f8d1ccdf266f23124f9c7b5af416a9 Mon Sep 17 00:00:00 2001 From: Julien Coolen Date: Tue, 19 May 2026 11:00:52 +0100 Subject: [PATCH 11/19] Forward gas to EIP-2537 precompiles --- docs/audit.md | 13 +++- .../docs/plans/TESTING_STRATEGY.md | 8 ++- .../reference/ASKAMA_TEMPLATE_RUST_MAPPING.md | 2 +- .../reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md | 33 +++++----- .../src/lowering/artifacts.rs | 19 +----- .../solidity-verifier/src/lowering/kzg/mod.rs | 19 ++---- .../src/lowering/layout/memory.rs | 49 +++++++++++++-- .../src/lowering/layout/mod.rs | 56 +---------------- .../src/lowering/render/models.rs | 36 ++--------- .../solidity-verifier/src/lowering/tests.rs | 61 +++++++++---------- proofs/solidity-verifier/src/test.rs | 12 ++-- .../partials/verifier/AccumulatorHelpers.yul | 4 +- .../partials/verifier/AssemblyHelpers.yul | 2 +- .../partials/verifier/FinalPairing.yul | 8 +-- .../partials/verifier/PrecompileSmoke.sol | 20 ++++-- .../verifier/QuotientAndLinearization.yul | 2 +- 16 files changed, 148 insertions(+), 196 deletions(-) diff --git a/docs/audit.md b/docs/audit.md index 62c2c0054..ca9aceee8 100644 --- a/docs/audit.md +++ b/docs/audit.md @@ -17,7 +17,7 @@ The main findings are: | M-01 | Medium | Production verifier contains active `gas_checkpoint` `LOG1` instrumentation | | M-02 | Medium | Deployment smoke test does not prove MCOPY support or full EIP-2537 semantic compatibility | | M-03 | Medium / High on custom chains | Proof-point validation relies on generated "all absorbed points are later consumed by precompiles" invariant | -| M-04 | Medium | Hard-coded precompile gas caps can brick verification on target chains with different gas schedules | +| M-04 | Medium | Resolved: generated EIP-2537 calls forward `gas()` and constructor smoke covers full-size precompile shapes | | H-INT | High integration risk | Raw verifier does not bind proof meaning to an application, chain, contract, user, nullifier, or program domain | | L-01 | Low | Invalid public instances are range-checked but failure is deferred until after transcript work | | L-02 | Low | VK header values are not cross-checked against verifier constants after `extcodecopy` | @@ -93,7 +93,12 @@ From a static pass, the current pasted plan appears to route proof G1 material i ### M-04: Hard-coded precompile gas caps are brittle -The verifier uses fixed gas caps such as: +Status: Fixed in the generator. EIP-2537 calls now forward `gas()` instead of +rendering EIP-2537 gas-schedule literals, and the constructor smoke test +exercises the largest generated G1MSM input and the runtime two-pair pairing +shape with identity data. + +Previously, the verifier used fixed gas caps such as: ```solidity staticcall(575096, 0x0c, 0xa300, 0x30c0, FINAL_COM_MPTR, 0x80) @@ -102,7 +107,9 @@ staticcall(62000, 0x0c, ..., 0xa0, ..., 0x80) staticcall(50000, 0x0b, ..., 0x0100, ..., 0x80) ``` -These are probably tuned for a specific EIP-2537 gas schedule. But if the verifier is deployed to a chain with a different gas schedule, modified precompile pricing, or less favorable implementation, valid proofs can revert. +Those caps were tuned for a specific EIP-2537 gas schedule. If such verifier +bytecode is deployed to a chain with a different gas schedule, modified +precompile pricing, or less favorable implementation, valid proofs can revert. **Impact:** Availability failure for valid proofs on target chains/forks with different precompile gas costs. diff --git a/proofs/solidity-verifier/docs/plans/TESTING_STRATEGY.md b/proofs/solidity-verifier/docs/plans/TESTING_STRATEGY.md index 8277b9f12..c5743ebd3 100644 --- a/proofs/solidity-verifier/docs/plans/TESTING_STRATEGY.md +++ b/proofs/solidity-verifier/docs/plans/TESTING_STRATEGY.md @@ -499,7 +499,7 @@ checkpoint. | Empty/edge circuit shapes | Shape fuzz circuits with no advice in a phase, no lookups, one lookup, additive selectors, complex selectors, next rotations, second phase advice, permutation on/off, and wide advice counts that stress memory layout. | | PCS/KZG/quotient | Mutate every quotient commitment, proof eval, opening proof, batching scalar source, quotient evaluator output, and external quotient return length. Assert the final pairing result is semantically checked, not just precompile call success. | | Accumulator-specific | Check accumulator schema consumes exactly the expected public input words. Test unused high limb bits, malformed identity encoding, x/y limb swaps, scalar mutation, zero/identity accumulator cases, and any future fixed-base tail. | -| Precompile/fail behavior | Constructor smoke tests cover MCOPY and EIP-2537 identity calls; add tests for short return data, false pairing result, reverted precompile call, and stale return memory using generated-template mutations or a helper harness. | +| Precompile/fail behavior | Constructor smoke tests cover MCOPY, the largest generated G1MSM input, and the two-pair EIP-2537 pairing shape; add tests for short return data, false pairing result, reverted precompile call, and stale return memory using generated-template mutations or a helper harness. | | Memory/layout | Fast generator tests should assert no overlap between VK, challenge, transcript, quotient, PCS, accumulator, and scratch regions. Keep these as compile-time/layout tests in `src/codegen/mod.rs` and `src/codegen/template.rs`. | | Production artifact checks | `verifyProof` production renders stay `external view`, no `LOG1`, no gas checkpoints, Solidity pragma `^0.8.24`, Cancun/Prague target, runtime size below EIP-170 with margin. | | Wrapper/application binding | Add small mock wrapper contracts that bind expected state root, program ID, chain/domain, caller/action hash, nullifier/nonce. Same proof with wrong wrapper context must reject. | @@ -586,7 +586,8 @@ Existing coverage already includes a healthy baseline: - Separate VK pinning and VK payload mutation. - Pinned quotient dependency checks. - Malformed calldata rejection. -- MCOPY/EIP-2537 constructor smoke tests. +- MCOPY/EIP-2537 constructor smoke tests, including full-size MSM and two-pair + pairing shapes. - Production render checks for `external view` and no gas logs. - Native/Solidity trace equivalence. - Scalar canonicality tests for proof scalars. @@ -618,7 +619,8 @@ A verifier profile is considered covered when: 5. EIP-2537 integration tests cover call failure, short return, semantic false return, and valid precompile behavior. 6. Deployment CI exercises the exact destination chain or fork configuration, - including constructor-time MCOPY/EIP-2537 smoke tests. + including constructor-time MCOPY/EIP-2537 smoke tests for the largest + generated precompile shapes. 7. Production artifacts compile with the pinned compiler/EVM target and stay within size limits. 8. Raw verifier NatSpec documents that application contracts must bind diff --git a/proofs/solidity-verifier/docs/reference/ASKAMA_TEMPLATE_RUST_MAPPING.md b/proofs/solidity-verifier/docs/reference/ASKAMA_TEMPLATE_RUST_MAPPING.md index efceaa81e..39f58aca9 100644 --- a/proofs/solidity-verifier/docs/reference/ASKAMA_TEMPLATE_RUST_MAPPING.md +++ b/proofs/solidity-verifier/docs/reference/ASKAMA_TEMPLATE_RUST_MAPPING.md @@ -398,7 +398,7 @@ comparison against the instrumented Rust verifier. | Fewer point sets / dummy queries | `pcs.rs::compute_dummy_queries` | Can reduce KZG point-set work for selected profiles | Proof layout and transcript must include matching dummy evals | | Truncated PCS challenges | `truncated-challenges` feature | Mirrors Midfall KZG challenge truncation where enabled | Only the specified challenges/powers are truncated | | Accumulator pairing batch | `Halo2Verifier.sol` accumulator section | Combines public accumulator pairing with final KZG pairing | Batch randomizer is derived after all four G1 inputs are fixed | -| EIP-2537 gas caps and return-size checks | `TemplateConstants`, `layout::precompile::*_gas_cap` | Catches missing or incompatible precompiles without a runtime gas-table helper | Gas constants must match target fork assumptions | +| EIP-2537 gas forwarding and return-size checks | `TemplateConstants`, `layout::precompile` addresses | Forwards `gas()` to avoid chain-specific gas caps while checking call success and exact return sizes | Deployment smoke tests must pass on the target fork/chain | | Gas checkpoints | `RenderDiagnostics { gas_checkpoints: true, .. }` | Gives stable section-level gas deltas | Not a `view` verifier; profiling only | ## Trace Coverage diff --git a/proofs/solidity-verifier/docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md b/proofs/solidity-verifier/docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md index 39e363102..14d1ef4a8 100644 --- a/proofs/solidity-verifier/docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md +++ b/proofs/solidity-verifier/docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md @@ -66,10 +66,10 @@ proofs using: Supported execution target: an Ethereum-compatible Cancun-or-newer EVM with `MCOPY` and Prague/EIP-2537 BLS12-381 precompiles at exactly the addresses above, implementing the EIP-2537 input encodings, subgroup checks, return sizes, -and gas schedule. The repository CI/dev runner exercises this target through -Prague-spec `revm`; deployers on L2s, forks, or alt-EVMs must run the same -precompile conformance tests against their target chain before treating the -verifier as production-safe. +and enough gas capacity for the generated full-size calls. The repository +CI/dev runner exercises this target through Prague-spec `revm`; deployers on +L2s, forks, or alt-EVMs must run the same precompile conformance tests against +their target chain before treating the verifier as production-safe. The generated verifier is not a generic reusable verifier. It is circuit-specialized. Circuit metadata, proof read order, quotient identity @@ -1551,14 +1551,15 @@ The generated verifier constructor runs smoke tests: - `MCOPY` one-word round trip in constructor scratch. - `G1ADD(identity, identity) -> identity`. -- `G1MSM([(identity, 0)]) -> identity`. -- `PAIRING_CHECK([(identity_g1, identity_g2)]) -> true`. +- Largest generated `G1MSM` input with identity/zero terms -> identity. +- `PAIRING_CHECK` over two identity `(G1, G2)` pairs -> true. Deploy only on forks/chains where EIP-2537 and `MCOPY` are available with the -exact addresses, encodings, subgroup checks, return-size behavior, and gas -schedule above. The test runner uses Prague-spec `revm` and includes direct -precompile conformance coverage for malformed G1 rejection, non-identity G1ADD, -two-term and 78-term G1MSM, true and false pairings, and pairing bilinearity. +exact addresses, encodings, subgroup checks, return-size behavior, and enough +gas capacity for the generated full-size calls. The test runner uses +Prague-spec `revm` and includes direct precompile conformance coverage for +malformed G1 rejection, non-identity G1ADD, two-term and 78-term G1MSM, true +and false pairings, and pairing bilinearity. Every EIP-2537 call checks: @@ -1566,13 +1567,11 @@ Every EIP-2537 call checks: - Exact return-data size. - For pairing, returned word is 1. -Gas caps are generated as literals instead of forwarding all remaining gas or -carrying the EIP-2537 discount table in runtime bytecode: - -- G1ADD cap: `50000`. -- G1MSM cap: `50000 + k * discount[k] * 12000 / 1000`, with the EIP-2537 - discount table for `k <= 128`. -- Pairing cap: `50000 + 60000 * num_pairs`. +EIP-2537 calls forward `gas()` rather than rendering chain-specific gas caps. +The constructor also smoke-tests the largest generated G1MSM input length and +the runtime two-pair KZG pairing input size with identity data, so deployment +fails early when the target chain/fork cannot execute the verifier's +worst-case precompile shapes under the supplied deployment gas. ## 16. Codegen Configuration diff --git a/proofs/solidity-verifier/src/lowering/artifacts.rs b/proofs/solidity-verifier/src/lowering/artifacts.rs index 8d26797d4..3830f3f51 100644 --- a/proofs/solidity-verifier/src/lowering/artifacts.rs +++ b/proofs/solidity-verifier/src/lowering/artifacts.rs @@ -205,20 +205,6 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { }) .unwrap_or((false, 0, 0, 0, false)); - let g1msm_single_gas_cap = layout::precompile::g1msm_gas_cap(layout::G1_MSM_PAIR_BYTES); - let lin_trace_g1msm_gas_cap = layout::precompile::g1msm_gas_cap( - (meta.num_quotients + sorted_simple.len()) * layout::G1_MSM_PAIR_BYTES, - ); - // The accumulator RHS MSM always includes the carried RHS point and may - // append generated fixed bases whose public scalars are nonzero. A max - // cap is safe because unused precompile gas is returned, and it avoids - // rendering the EIP-2537 discount-table switch into runtime bytecode. - let acc_rhs_g1msm_gas_cap = layout::precompile::g1msm_gas_cap( - (1 + acc_fixed_bases.len()) * layout::G1_MSM_PAIR_BYTES, - ); - let final_pairing_gas_cap = - layout::precompile::pairing_gas_cap(layout::PAIRING_TWO_PAIR_BYTES); - let verifier = Halo2Verifier { template_constants: Default::default(), trace, @@ -226,10 +212,7 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { quotient_pow5_helper: quotient_helper_flags.pow5, quotient_limb7_helper: quotient_helper_flags.limb7, quotient_wide_limb7_helper: quotient_helper_flags.wide_limb7, - g1msm_single_gas_cap, - lin_trace_g1msm_gas_cap, - acc_rhs_g1msm_gas_cap, - final_pairing_gas_cap, + constructor_g1msm_smoke_input_bytes: plan.memory.constructor_g1msm_smoke_input_bytes, limb7_yul_coeffs: LIMB7_YUL_COEFFS, wide_limb7_yul_coeffs: WIDE_LIMB7_YUL_COEFFS, fr_delta: fr_delta_literal(), diff --git a/proofs/solidity-verifier/src/lowering/kzg/mod.rs b/proofs/solidity-verifier/src/lowering/kzg/mod.rs index 0bd66decf..0fb662402 100644 --- a/proofs/solidity-verifier/src/lowering/kzg/mod.rs +++ b/proofs/solidity-verifier/src/lowering/kzg/mod.rs @@ -46,7 +46,6 @@ use crate::lowering::{ abi::proof::ProofCalldataLayout, encoding::{ConstraintSystemMeta, Data, EcPoint, Location, Ptr, Word}, layout::{ - self, memory::{ FinalMsmShape, PcsMemoryRequirements, VerifierMemoryLayout, G1ADD_INPUT_BYTES, G1_BYTES, G1_MSM_PAIR_BYTES, PCS_STATIC_WORKING_WORDS, WORD_BYTES, @@ -1265,9 +1264,8 @@ pub(crate) fn computations( "q_com trace MSM input term count changed during emission" ); let msm_len = non_identity_terms * G1_MSM_PAIR_BYTES; - let msm_gas_cap = layout::precompile::g1msm_gas_cap(msm_len); lines.push(format!( - "let q_com_trace_ok_{set_idx} := staticcall({msm_gas_cap}, 0x0c, {trace_scratch:#x}, {msm_len:#x}, {trace_scratch:#x}, {G1_BYTES:#x})" + "let q_com_trace_ok_{set_idx} := staticcall(gas(), 0x0c, {trace_scratch:#x}, {msm_len:#x}, {trace_scratch:#x}, {G1_BYTES:#x})" )); lines.push(format!( "q_com_trace_ok_{set_idx} := and(q_com_trace_ok_{set_idx}, eq(returndatasize(), {G1_BYTES:#x}))" @@ -1675,10 +1673,9 @@ pub(crate) fn computations( "final MSM input term count changed during emission" ); - let final_msm_gas_cap = layout::precompile::g1msm_gas_cap(final_msm_len); lines.push("if success {".to_string()); lines.push(format!( - " success := staticcall({final_msm_gas_cap}, 0x0c, {final_msm_scratch:#x}, {:#x}, FINAL_COM_MPTR, {G1_BYTES:#x})", + " success := staticcall(gas(), 0x0c, {final_msm_scratch:#x}, {:#x}, FINAL_COM_MPTR, {G1_BYTES:#x})", final_msm_len )); lines.push(format!( @@ -1724,8 +1721,7 @@ pub(crate) fn computations( )); lines.push("if success {".to_string()); lines.push(format!( - " success := staticcall({}, 0x0c, {scratch:#x}, {G1_MSM_PAIR_BYTES:#x}, {scratch:#x}, {G1_BYTES:#x})", - layout::precompile::g1msm_gas_cap(G1_MSM_PAIR_BYTES) + " success := staticcall(gas(), 0x0c, {scratch:#x}, {G1_MSM_PAIR_BYTES:#x}, {scratch:#x}, {G1_BYTES:#x})" )); lines.push(format!( " success := and(success, eq(returndatasize(), {G1_BYTES:#x}))" @@ -1738,8 +1734,7 @@ pub(crate) fn computations( )); lines.push("if success {".to_string()); lines.push(format!( - " success := staticcall({}, 0x0b, {scratch:#x}, {G1ADD_INPUT_BYTES:#x}, {scratch:#x}, {G1_BYTES:#x})", - layout::precompile::G1ADD_GAS_CAP + " success := staticcall(gas(), 0x0b, {scratch:#x}, {G1ADD_INPUT_BYTES:#x}, {scratch:#x}, {G1_BYTES:#x})" )); lines.push(format!( " success := and(success, eq(returndatasize(), {G1_BYTES:#x}))" @@ -1751,8 +1746,7 @@ pub(crate) fn computations( lines.push(format!("mstore({scratch_g1add_scalar:#x}, mload(X3_MPTR))")); lines.push("if success {".to_string()); lines.push(format!( - " success := staticcall({}, 0x0c, {scratch_g1_b:#x}, {G1_MSM_PAIR_BYTES:#x}, {scratch_g1_b:#x}, {G1_BYTES:#x})", - layout::precompile::g1msm_gas_cap(G1_MSM_PAIR_BYTES) + " success := staticcall(gas(), 0x0c, {scratch_g1_b:#x}, {G1_MSM_PAIR_BYTES:#x}, {scratch_g1_b:#x}, {G1_BYTES:#x})" )); lines.push(format!( " success := and(success, eq(returndatasize(), {G1_BYTES:#x}))" @@ -1760,8 +1754,7 @@ pub(crate) fn computations( lines.push("}".to_string()); lines.push("if success {".to_string()); lines.push(format!( - " success := staticcall({}, 0x0b, {scratch:#x}, {G1ADD_INPUT_BYTES:#x}, {scratch:#x}, {G1_BYTES:#x})", - layout::precompile::G1ADD_GAS_CAP + " success := staticcall(gas(), 0x0b, {scratch:#x}, {G1ADD_INPUT_BYTES:#x}, {scratch:#x}, {G1_BYTES:#x})" )); lines.push(format!( " success := and(success, eq(returndatasize(), {G1_BYTES:#x}))" diff --git a/proofs/solidity-verifier/src/lowering/layout/memory.rs b/proofs/solidity-verifier/src/lowering/layout/memory.rs index 30cfe4cd7..55bc5acb2 100644 --- a/proofs/solidity-verifier/src/lowering/layout/memory.rs +++ b/proofs/solidity-verifier/src/lowering/layout/memory.rs @@ -22,7 +22,7 @@ use std::collections::BTreeMap; pub(crate) use crate::lowering::layout::{ ACC_MSM_MIN_SCRATCH_BYTES, G1ADD_INPUT_BYTES, G1_BYTES, G1_MSM_PAIR_BYTES, G1_WORDS, - LOW_MEMORY_SCRATCH_START, MODEXP_FRAME_BYTES, MODEXP_SCRATCH_BYTES, PAIRING_PAIR_BYTES, + LOW_MEMORY_SCRATCH_START, MODEXP_FRAME_BYTES, MODEXP_SCRATCH_BYTES, PAIRING_STATIC_WORKING_WORDS, PAIRING_TWO_PAIR_BYTES, PCS_PAIRING_SCRATCH_START, PCS_STATIC_WORKING_WORDS, QUOTIENT_RETURN_BUFFER_START, SOLIDITY_FREE_MEMORY_POINTER_SLOT, SOLIDITY_RESERVED_MEMORY_BYTES, SOLIDITY_SCRATCH_SPACE_BYTES, SOLIDITY_ZERO_SLOT, @@ -419,6 +419,10 @@ pub(crate) struct VerifierMemoryLayout { pub(crate) theta_windows: ThetaWindowLayout, /// Fixed low-memory scratch for constructor EIP-2537 smoke checks. pub(crate) constructor_smoke_scratch_mptr: usize, + /// Constructor-only scratch for the largest generated G1MSM smoke check. + pub(crate) constructor_g1msm_smoke_scratch_mptr: usize, + /// Byte length exercised by the constructor's largest G1MSM smoke check. + pub(crate) constructor_g1msm_smoke_input_bytes: usize, /// Base of the streaming transcript buffer. pub(crate) transcript_mptr: usize, /// Low-memory scratch used by the PCS pairing-preparation helper. @@ -588,6 +592,17 @@ impl VerifierMemoryLayout { let q_com_trace_len = config.pcs.q_com_trace_msm.input_bytes; let final_msm_len = config.pcs.final_msm.input_bytes; let acc_msm_len = config.acc_msm_terms * G1_MSM_PAIR_BYTES; + let lin_trace_len = (meta.num_quotients + meta.num_simple_selectors) * G1_MSM_PAIR_BYTES; + let constructor_g1msm_smoke_len = [ + G1_MSM_PAIR_BYTES, + q_com_trace_len, + final_msm_len, + acc_msm_len, + lin_trace_len, + ] + .into_iter() + .max() + .expect("constructor G1MSM smoke bounds are non-empty"); let batch_invert_len = batch_invert_scratch_bytes(meta, config.num_instances); let quotient_return_len = (2 + meta.num_simple_selectors) * WORD_BYTES; @@ -599,7 +614,7 @@ impl VerifierMemoryLayout { let constructor_smoke_scratch_mptr = arena.alloc_phase_scratch( "constructor_smoke_scratch", LOW_MEMORY_SCRATCH_START, - PAIRING_PAIR_BYTES, + PAIRING_TWO_PAIR_BYTES, MemoryPhase::ConstructorSmoke, ); let transcript_mptr = arena.alloc_phase_scratch( @@ -743,6 +758,12 @@ impl VerifierMemoryLayout { // Decompressed proof commitments are stored contiguously by category. // Everything after this point is either selector state or scratch. let after_comms = comms_mptr_base.value().as_usize() + committed_g1s * G1_BYTES; + let constructor_g1msm_smoke_scratch_mptr = arena.alloc_phase_scratch( + "constructor_g1msm_smoke_scratch", + after_comms.next_multiple_of(WORD_BYTES), + constructor_g1msm_smoke_len, + MemoryPhase::ConstructorSmoke, + ); let selector_acc_mptr = arena.alloc_after( "selector_accumulators", comms_mptr_base.value().as_usize(), @@ -813,7 +834,7 @@ impl VerifierMemoryLayout { // one-word scratch is placed after every registered region rather than // borrowing any phase-specific base. let trace_u256_mptr = [ - constructor_smoke_scratch_mptr + PAIRING_PAIR_BYTES, + constructor_smoke_scratch_mptr + PAIRING_TWO_PAIR_BYTES, transcript_mptr + config.transcript_words * WORD_BYTES, pcs_pairing_scratch_mptr + PCS_STATIC_WORKING_WORDS * WORD_BYTES, verifier_return_mptr + WORD_BYTES, @@ -832,6 +853,7 @@ impl VerifierMemoryLayout { pcs_q_eval_source_table_mptr + q_eval_source_len, pcs_q_com_trace_scratch_mptr + q_com_trace_len, pcs_final_msm_scratch_mptr + final_msm_len, + constructor_g1msm_smoke_scratch_mptr + constructor_g1msm_smoke_len, acc_msm_scratch + acc_msm_len, accumulator_pairing_batch_mptr + ACCUMULATOR_PAIRING_BATCH_BYTES, ] @@ -850,6 +872,8 @@ impl VerifierMemoryLayout { map: arena.into_map(), theta_windows, constructor_smoke_scratch_mptr, + constructor_g1msm_smoke_scratch_mptr, + constructor_g1msm_smoke_input_bytes: constructor_g1msm_smoke_len, transcript_mptr, pcs_pairing_scratch_mptr, verifier_return_mptr, @@ -1276,7 +1300,7 @@ mod tests { "constructor_smoke_scratch", layout.constructor_smoke_scratch_mptr, LOW_MEMORY_SCRATCH_START, - PAIRING_PAIR_BYTES, + PAIRING_TWO_PAIR_BYTES, ), ( "transcript_buffer", @@ -1323,6 +1347,23 @@ mod tests { assert_eq!(region.len, len, "{name} len drifted"); } + let constructor_msm = layout + .map + .region("constructor_g1msm_smoke_scratch") + .expect("constructor G1MSM smoke scratch registered"); + assert_eq!( + layout.constructor_g1msm_smoke_scratch_mptr, + constructor_msm.start + ); + assert_eq!( + constructor_msm.len, + layout.constructor_g1msm_smoke_input_bytes + ); + assert_eq!( + layout.constructor_g1msm_smoke_input_bytes, + meta.num_simple_selectors * G1_MSM_PAIR_BYTES + ); + let scalar_inv = layout .map .region("scalar_inv_scratch") diff --git a/proofs/solidity-verifier/src/lowering/layout/mod.rs b/proofs/solidity-verifier/src/lowering/layout/mod.rs index b9f7f24a7..1c885ea67 100644 --- a/proofs/solidity-verifier/src/lowering/layout/mod.rs +++ b/proofs/solidity-verifier/src/lowering/layout/mod.rs @@ -92,8 +92,8 @@ pub(crate) const PAIRING_STATIC_WORKING_WORDS: usize = PAIRING_TWO_PAIR_BYTES / pub(crate) const FINAL_PAIRING_SCRATCH_START: usize = PAIRING_TWO_PAIR_BYTES; pub(crate) mod precompile { - //! EVM precompile addresses, frame lengths, and gas constants used by the - //! generated verifier. These values come from EIP-198 and EIP-2537; keep + //! EVM precompile addresses used by the generated verifier. These values + //! come from EIP-198 and EIP-2537; keep //! template call sites wired through these names so future fork changes are //! not hidden in hand-written Yul literals. @@ -105,55 +105,6 @@ pub(crate) mod precompile { pub(crate) const G1MSM_ADDRESS: usize = 0x0c; /// Prague/EIP-2537 BLS12-381 pairing precompile. pub(crate) const PAIRING_ADDRESS: usize = 0x0f; - - /// Deployment smoke-test gas caps. They are intentionally above the single - /// operation costs, but still bounded enough to catch missing precompiles. - pub(crate) const G1ADD_GAS_CAP: usize = 50_000; - /// Gas cap for the deployment-time identity G1MSM smoke test. - pub(crate) const G1MSM_SMOKE_GAS_CAP: usize = 60_000; - /// Gas cap for the deployment-time identity pairing smoke test. - pub(crate) const PAIRING_SMOKE_GAS_CAP: usize = 120_000; - /// EIP-2537 G1MSM gas formula: `base + k * discount[k] * mul_cost / 1000`. - pub(crate) const G1MSM_BASE_GAS: usize = 50_000; - /// Per-scalar multiplication cost before applying the EIP-2537 discount. - pub(crate) const G1MSM_SCALAR_MULTIPLICATION_COST: usize = 12_000; - /// Denominator used by the EIP-2537 discount table. - pub(crate) const G1MSM_DISCOUNT_DENOMINATOR: usize = 1_000; - /// EIP-2537 pairing gas formula: base + pair_count * pair_cost. - pub(crate) const PAIRING_BASE_GAS: usize = 50_000; - pub(crate) const PAIRING_PAIR_GAS: usize = 60_000; - - /// EIP-2537 G1MSM discount table for k = 0..128. - /// - /// k=0 is not a real precompile input shape for verifier calls, but keeping - /// it in the table preserves the previous Yul helper's behavior. - pub(crate) const G1MSM_DISCOUNT_TABLE: [usize; 129] = [ - 0, 1000, 949, 848, 797, 764, 750, 738, 728, 719, 712, 705, 698, 692, 687, 682, 677, 673, - 669, 665, 661, 658, 654, 651, 648, 645, 642, 640, 637, 635, 632, 630, 627, 625, 623, 621, - 619, 617, 615, 613, 611, 609, 608, 606, 604, 603, 601, 599, 598, 596, 595, 593, 592, 591, - 589, 588, 586, 585, 584, 582, 581, 580, 579, 577, 576, 575, 574, 573, 572, 570, 569, 568, - 567, 566, 565, 564, 563, 562, 561, 560, 559, 558, 557, 556, 555, 554, 553, 552, 551, 550, - 549, 548, 547, 547, 546, 545, 544, 543, 542, 541, 540, 540, 539, 538, 537, 536, 536, 535, - 534, 533, 532, 532, 531, 530, 529, 528, 528, 527, 526, 525, 525, 524, 523, 522, 522, 521, - 520, 520, 519, - ]; - - /// Return the bounded gas cap for a G1MSM input of `input_len` bytes. - /// - /// Pinned verifier codegen knows each static MSM input length, so rendering - /// this value as a literal avoids carrying the whole discount-table switch - /// in deployed Yul. - pub(crate) fn g1msm_gas_cap(input_len: usize) -> usize { - let k = input_len / super::G1_MSM_PAIR_BYTES; - let discount = G1MSM_DISCOUNT_TABLE.get(k).copied().unwrap_or(519); - G1MSM_BASE_GAS - + k * discount * G1MSM_SCALAR_MULTIPLICATION_COST / G1MSM_DISCOUNT_DENOMINATOR - } - - /// Return the bounded gas cap for a pairing input of `input_len` bytes. - pub(crate) fn pairing_gas_cap(input_len: usize) -> usize { - PAIRING_BASE_GAS + (input_len / super::PAIRING_PAIR_BYTES) * PAIRING_PAIR_GAS - } } pub(crate) mod modexp_frame { @@ -739,9 +690,6 @@ mod tests { assert_eq!(precompile::G1ADD_ADDRESS, 0x0b); assert_eq!(precompile::G1MSM_ADDRESS, 0x0c); assert_eq!(precompile::PAIRING_ADDRESS, 0x0f); - assert_eq!(precompile::G1ADD_GAS_CAP, 50_000); - assert_eq!(precompile::G1MSM_SMOKE_GAS_CAP, 60_000); - assert_eq!(precompile::PAIRING_SMOKE_GAS_CAP, 120_000); assert_eq!(modexp_frame::BASE_LEN_OFFSET, 0x00); assert_eq!(modexp_frame::EXP_LEN_OFFSET, 0x20); assert_eq!(modexp_frame::MOD_LEN_OFFSET, 0x40); diff --git a/proofs/solidity-verifier/src/lowering/render/models.rs b/proofs/solidity-verifier/src/lowering/render/models.rs index 8d66f8c7f..532cbeddc 100644 --- a/proofs/solidity-verifier/src/lowering/render/models.rs +++ b/proofs/solidity-verifier/src/lowering/render/models.rs @@ -35,8 +35,6 @@ pub(crate) struct TemplateConstants { pub(crate) g1_msm_pair_bytes: usize, /// G1ADD input byte width. pub(crate) g1add_input_bytes: usize, - /// Pairing input byte width for one pair. - pub(crate) pairing_pair_bytes: usize, /// Pairing input byte width for two pairs. pub(crate) pairing_two_pair_bytes: usize, /// EIP-2537 precompile constants. @@ -49,15 +47,12 @@ pub(crate) struct TemplateConstants { pub(crate) quotient_vm: QuotientVmTemplateConstants, } -/// EIP-2537 precompile addresses and gas formulas rendered into templates. +/// EIP-2537 precompile addresses rendered into templates. #[derive(Clone, Copy, Debug)] pub(crate) struct Eip2537TemplateConstants { pub(crate) g1add_address: usize, pub(crate) g1msm_address: usize, pub(crate) pairing_address: usize, - pub(crate) g1add_gas_cap: usize, - pub(crate) g1msm_smoke_gas_cap: usize, - pub(crate) pairing_smoke_gas_cap: usize, pub(crate) smoke_scratch_bytes: usize, } @@ -157,16 +152,12 @@ impl Default for TemplateConstants { g1_bytes: layout::G1_BYTES, g1_msm_pair_bytes: layout::G1_MSM_PAIR_BYTES, g1add_input_bytes: layout::G1ADD_INPUT_BYTES, - pairing_pair_bytes: layout::PAIRING_PAIR_BYTES, pairing_two_pair_bytes: layout::PAIRING_TWO_PAIR_BYTES, eip2537: Eip2537TemplateConstants { g1add_address: layout::precompile::G1ADD_ADDRESS, g1msm_address: layout::precompile::G1MSM_ADDRESS, pairing_address: layout::precompile::PAIRING_ADDRESS, - g1add_gas_cap: layout::precompile::G1ADD_GAS_CAP, - g1msm_smoke_gas_cap: layout::precompile::G1MSM_SMOKE_GAS_CAP, - pairing_smoke_gas_cap: layout::precompile::PAIRING_SMOKE_GAS_CAP, - smoke_scratch_bytes: layout::PAIRING_PAIR_BYTES, + smoke_scratch_bytes: layout::PAIRING_TWO_PAIR_BYTES, }, modexp: ModexpTemplateConstants { address: layout::precompile::MODEXP_ADDRESS, @@ -431,14 +422,8 @@ pub(crate) struct Halo2Verifier { pub(crate) quotient_pow5_helper: bool, pub(crate) quotient_limb7_helper: bool, pub(crate) quotient_wide_limb7_helper: bool, - /// Generated gas cap for one-pair G1MSM calls. - pub(crate) g1msm_single_gas_cap: usize, - /// Generated gas cap for the trace-only linearization MSM. - pub(crate) lin_trace_g1msm_gas_cap: usize, - /// Generated max gas cap for the accumulator RHS MSM. - pub(crate) acc_rhs_g1msm_gas_cap: usize, - /// Generated gas cap for the final two-pair KZG pairing check. - pub(crate) final_pairing_gas_cap: usize, + /// Largest generated G1MSM input length, smoke-tested at deployment. + pub(crate) constructor_g1msm_smoke_input_bytes: usize, pub(crate) limb7_yul_coeffs: [&'static str; layout::quotient_limb::LIN_COEFFS], pub(crate) wide_limb7_yul_coeffs: [&'static str; layout::quotient_limb::LIN_COEFFS], pub(crate) fr_delta: String, @@ -1085,18 +1070,7 @@ mod tests { quotient_pow5_helper: false, quotient_limb7_helper: false, quotient_wide_limb7_helper: false, - g1msm_single_gas_cap: crate::lowering::layout::precompile::g1msm_gas_cap( - crate::lowering::layout::G1_MSM_PAIR_BYTES, - ), - lin_trace_g1msm_gas_cap: crate::lowering::layout::precompile::g1msm_gas_cap( - crate::lowering::layout::G1_MSM_PAIR_BYTES, - ), - acc_rhs_g1msm_gas_cap: crate::lowering::layout::precompile::g1msm_gas_cap( - crate::lowering::layout::G1_MSM_PAIR_BYTES, - ), - final_pairing_gas_cap: crate::lowering::layout::precompile::pairing_gas_cap( - crate::lowering::layout::PAIRING_TWO_PAIR_BYTES, - ), + constructor_g1msm_smoke_input_bytes: crate::lowering::layout::G1_MSM_PAIR_BYTES, limb7_yul_coeffs: crate::lowering::quotient_numerator::vm::LIMB7_YUL_COEFFS, wide_limb7_yul_coeffs: crate::lowering::quotient_numerator::vm::WIDE_LIMB7_YUL_COEFFS, fr_delta: crate::lowering::quotient_numerator::vm::fr_delta_literal(), diff --git a/proofs/solidity-verifier/src/lowering/tests.rs b/proofs/solidity-verifier/src/lowering/tests.rs index e1950b2b8..01a4eff00 100644 --- a/proofs/solidity-verifier/src/lowering/tests.rs +++ b/proofs/solidity-verifier/src/lowering/tests.rs @@ -926,38 +926,34 @@ fn transcript_memory_bound_handles_wide_bls_advice_phase() { } #[test] -fn eip2537_calls_use_bounded_gas_literals() { +fn eip2537_calls_forward_remaining_gas() { let verifier_template = verifier_template_corpus(); let pcs_codegen = include_str!("kzg/mod.rs"); - for source in [verifier_template, pcs_codegen] { - assert!( - !source.contains("staticcall(gas(), 0x0b"), - "G1ADD calls must use bounded gas caps" - ); - assert!( - !source.contains("staticcall(gas(), 0x0c"), - "G1MSM calls must use bounded gas caps" - ); - assert!( - !source.contains("staticcall(gas(), 0x0f"), - "pairing calls must use bounded gas caps" - ); - } - - assert!(!verifier_template.contains("function g1add_gas_cap()")); - assert!(!verifier_template.contains("function g1msm_gas_cap(input_len)")); - assert!(!verifier_template.contains("function pairing_gas_cap(input_len)")); assert!( - verifier_template.contains("{{ g1msm_single_gas_cap }}") - && verifier_template.contains("{{ final_pairing_gas_cap }}"), - "main verifier template should render generated gas-cap literals" + verifier_template + .contains("staticcall(gas(), {{ template_constants.eip2537.g1add_address|hex() }}") + && verifier_template + .contains("staticcall(gas(), {{ template_constants.eip2537.g1msm_address|hex() }}") + && verifier_template.contains( + "staticcall(gas(), {{ template_constants.eip2537.pairing_address|hex() }}" + ), + "main verifier template should forward remaining gas to EIP-2537 precompiles" ); assert!( - pcs_codegen.contains("layout::precompile::g1msm_gas_cap") - && pcs_codegen.contains("layout::precompile::G1ADD_GAS_CAP"), - "PCS emitter should compute static EIP-2537 caps at codegen time" + pcs_codegen.contains("staticcall(gas(), 0x0c") + && pcs_codegen.contains("staticcall(gas(), 0x0b"), + "PCS emitter should forward remaining gas to EIP-2537 precompiles" ); + for source in [verifier_template, pcs_codegen] { + assert!( + !source.contains("g1msm_gas_cap") + && !source.contains("G1ADD_GAS_CAP") + && !source.contains("PAIRING_SMOKE_GAS_CAP") + && !source.contains("final_pairing_gas_cap"), + "EIP-2537 gas-cap literals must not be rendered or computed" + ); + } } #[test] @@ -997,8 +993,8 @@ fn failed_success_paths_do_not_enter_ec_precompiles() { ); assert!( pcs_codegen.contains("if success {") - && pcs_codegen.contains("success := staticcall({final_msm_gas_cap}") - && pcs_codegen.contains("success := staticcall({}, 0x0b"), + && pcs_codegen.contains("success := staticcall(gas(), 0x0c") + && pcs_codegen.contains("success := staticcall(gas(), 0x0b"), "PCS emitter should guard final MSM/add precompile calls with if success" ); } @@ -1017,11 +1013,12 @@ fn verifier_constructor_smoke_tests_runtime_prerequisites() { "eq(mload(add(scratch, {{ template_constants.word_bytes|hex() }})), 0x1234)", "non-Cancun fork fails during deployment", "G1ADD(identity, identity) -> identity", - "G1MSM([(identity, 0)]) -> identity", - "PAIRING_CHECK([(identity_g1, identity_g2)]) -> true", - "template_constants.eip2537.g1add_gas_cap", - "template_constants.eip2537.g1msm_smoke_gas_cap", - "template_constants.eip2537.pairing_smoke_gas_cap", + "Worst-case generated G1MSM with all identity/zero terms", + "constructor_g1msm_smoke_input_bytes", + "PAIRING_CHECK([(identity_g1, identity_g2), (identity_g1, identity_g2)])", + "staticcall(gas(), {{ template_constants.eip2537.g1add_address|hex() }}", + "staticcall(gas(), {{ template_constants.eip2537.g1msm_address|hex() }}", + "staticcall(gas(), {{ template_constants.eip2537.pairing_address|hex() }}", "template_constants.eip2537.g1add_address", "template_constants.eip2537.g1msm_address", "template_constants.eip2537.pairing_address", diff --git a/proofs/solidity-verifier/src/test.rs b/proofs/solidity-verifier/src/test.rs index 5fb52090b..206b6a359 100644 --- a/proofs/solidity-verifier/src/test.rs +++ b/proofs/solidity-verifier/src/test.rs @@ -1154,18 +1154,18 @@ fn verifier_constructor_rejects_missing_or_mismatched_eip2537_precompiles() { for (name, needle, replacement) in [ ( "missing G1ADD precompile", - "staticcall(50000, 0x0b", - "staticcall(50000, 0x12", + "staticcall(gas(), 0x0b", + "staticcall(gas(), 0x12", ), ( "G1MSM routed to G1ADD", - "staticcall(60000, 0x0c", - "staticcall(60000, 0x0b", + "staticcall(gas(), 0x0c", + "staticcall(gas(), 0x0b", ), ( "pairing routed to G1MSM", - "staticcall(120000, 0x0f", - "staticcall(120000, 0x0c", + "staticcall(gas(), 0x0f", + "staticcall(gas(), 0x0c", ), ] { let verifier_solidity = replace_required_precompile_staticcall( diff --git a/proofs/solidity-verifier/templates/partials/verifier/AccumulatorHelpers.yul b/proofs/solidity-verifier/templates/partials/verifier/AccumulatorHelpers.yul index d872afb9d..3a5d5eb77 100644 --- a/proofs/solidity-verifier/templates/partials/verifier/AccumulatorHelpers.yul +++ b/proofs/solidity-verifier/templates/partials/verifier/AccumulatorHelpers.yul @@ -278,7 +278,7 @@ // Single-pair MSM output overwrites ACC_LHS_MPTR with // lhs_scalar * decoded_lhs. If lhs_scalar is one, this // is also a curve/subgroup validation round-trip. - out := staticcall({{ g1msm_single_gas_cap }}, {{ template_constants.eip2537.g1msm_address|hex() }}, acc_scratch, {{ template_constants.g1_msm_pair_bytes|hex() }}, ACC_LHS_MPTR, {{ template_constants.g1_bytes|hex() }}) + out := staticcall(gas(), {{ template_constants.eip2537.g1msm_address|hex() }}, acc_scratch, {{ template_constants.g1_msm_pair_bytes|hex() }}, ACC_LHS_MPTR, {{ template_constants.g1_bytes|hex() }}) out := and(out, eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) } } @@ -380,7 +380,7 @@ // The precompile also validates every nonzero fixed // base embedded by codegen and the carried RHS point. out := staticcall( - {{ acc_rhs_g1msm_gas_cap }}, + gas(), {{ template_constants.eip2537.g1msm_address|hex() }}, acc_scratch, acc_msm_len, diff --git a/proofs/solidity-verifier/templates/partials/verifier/AssemblyHelpers.yul b/proofs/solidity-verifier/templates/partials/verifier/AssemblyHelpers.yul index 4b704dd3c..f3ddbfc73 100644 --- a/proofs/solidity-verifier/templates/partials/verifier/AssemblyHelpers.yul +++ b/proofs/solidity-verifier/templates/partials/verifier/AssemblyHelpers.yul @@ -224,7 +224,7 @@ mcopy(add(scratch, 0x80), G2_BASE_MPTR, 0x100) mcopy(add(scratch, 0x180), rhs_mptr, 0x80) mcopy(add(scratch, 0x200), NEG_S_G2_BASE_MPTR, 0x100) - ret := staticcall({{ final_pairing_gas_cap }}, {{ template_constants.eip2537.pairing_address|hex() }}, scratch, {{ template_constants.pairing_two_pair_bytes|hex() }}, scratch, {{ template_constants.word_bytes|hex() }}) + ret := staticcall(gas(), {{ template_constants.eip2537.pairing_address|hex() }}, scratch, {{ template_constants.pairing_two_pair_bytes|hex() }}, scratch, {{ template_constants.word_bytes|hex() }}) ret := and(ret, eq(returndatasize(), {{ template_constants.word_bytes|hex() }})) ret := and(ret, mload(scratch)) if iszero(ret) { revert(0, 0) } diff --git a/proofs/solidity-verifier/templates/partials/verifier/FinalPairing.yul b/proofs/solidity-verifier/templates/partials/verifier/FinalPairing.yul index fd7e2a747..2d27318c9 100644 --- a/proofs/solidity-verifier/templates/partials/verifier/FinalPairing.yul +++ b/proofs/solidity-verifier/templates/partials/verifier/FinalPairing.yul @@ -33,12 +33,12 @@ mcopy(batch_ptr, ACC_RHS_MPTR, {{ template_constants.g1_bytes|hex() }}) mstore(add(batch_ptr, {{ template_constants.g1_bytes|hex() }}), acc_pair_alpha) if success { - success := staticcall({{ g1msm_single_gas_cap }}, {{ template_constants.eip2537.g1msm_address|hex() }}, batch_ptr, {{ template_constants.g1_msm_pair_bytes|hex() }}, batch_ptr, {{ template_constants.g1_bytes|hex() }}) + success := staticcall(gas(), {{ template_constants.eip2537.g1msm_address|hex() }}, batch_ptr, {{ template_constants.g1_msm_pair_bytes|hex() }}, batch_ptr, {{ template_constants.g1_bytes|hex() }}) success := and(success, eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) } mcopy(add(batch_ptr, {{ template_constants.g1_bytes|hex() }}), PAIRING_RHS_MPTR, {{ template_constants.g1_bytes|hex() }}) if success { - success := staticcall({{ template_constants.eip2537.g1add_gas_cap }}, {{ template_constants.eip2537.g1add_address|hex() }}, batch_ptr, {{ template_constants.g1add_input_bytes|hex() }}, PAIRING_RHS_MPTR, {{ template_constants.g1_bytes|hex() }}) + success := staticcall(gas(), {{ template_constants.eip2537.g1add_address|hex() }}, batch_ptr, {{ template_constants.g1add_input_bytes|hex() }}, PAIRING_RHS_MPTR, {{ template_constants.g1_bytes|hex() }}) success := and(success, eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) } @@ -47,12 +47,12 @@ mcopy(batch_ptr, ACC_LHS_MPTR, {{ template_constants.g1_bytes|hex() }}) mstore(add(batch_ptr, {{ template_constants.g1_bytes|hex() }}), acc_pair_alpha) if success { - success := staticcall({{ g1msm_single_gas_cap }}, {{ template_constants.eip2537.g1msm_address|hex() }}, batch_ptr, {{ template_constants.g1_msm_pair_bytes|hex() }}, batch_ptr, {{ template_constants.g1_bytes|hex() }}) + success := staticcall(gas(), {{ template_constants.eip2537.g1msm_address|hex() }}, batch_ptr, {{ template_constants.g1_msm_pair_bytes|hex() }}, batch_ptr, {{ template_constants.g1_bytes|hex() }}) success := and(success, eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) } mcopy(add(batch_ptr, {{ template_constants.g1_bytes|hex() }}), PAIRING_LHS_MPTR, {{ template_constants.g1_bytes|hex() }}) if success { - success := staticcall({{ template_constants.eip2537.g1add_gas_cap }}, {{ template_constants.eip2537.g1add_address|hex() }}, batch_ptr, {{ template_constants.g1add_input_bytes|hex() }}, PAIRING_LHS_MPTR, {{ template_constants.g1_bytes|hex() }}) + success := staticcall(gas(), {{ template_constants.eip2537.g1add_address|hex() }}, batch_ptr, {{ template_constants.g1add_input_bytes|hex() }}, PAIRING_LHS_MPTR, {{ template_constants.g1_bytes|hex() }}) success := and(success, eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) } } diff --git a/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol b/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol index 860b623e2..baa6e6180 100644 --- a/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol +++ b/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol @@ -21,25 +21,33 @@ // G1ADD(identity, identity) -> identity, 128-byte return. // This catches chains where the precompile is missing or returns a // non-standard success shape. - if iszero(staticcall({{ template_constants.eip2537.g1add_gas_cap }}, {{ template_constants.eip2537.g1add_address|hex() }}, scratch, {{ template_constants.g1add_input_bytes|hex() }}, scratch, {{ template_constants.g1_bytes|hex() }})) { revert(0, 0) } + if iszero(staticcall(gas(), {{ template_constants.eip2537.g1add_address|hex() }}, scratch, {{ template_constants.g1add_input_bytes|hex() }}, scratch, {{ template_constants.g1_bytes|hex() }})) { revert(0, 0) } if iszero(eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) { revert(0, 0) } if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { revert(0, 0) } - // G1MSM([(identity, 0)]) -> identity, 128-byte return. + // Worst-case generated G1MSM with all identity/zero terms -> + // identity, 128-byte return. This exercises the largest MSM input + // length rendered by this verifier instead of only a one-pair + // smoke call. + let msm_scratch := {{ memory.constructor_g1msm_smoke_scratch_mptr|hex() }} + for { let off := 0 } lt(off, {{ constructor_g1msm_smoke_input_bytes|hex() }}) { off := add(off, {{ template_constants.word_bytes|hex() }}) } { + mstore(add(msm_scratch, off), 0) + } // The production verifier uses G1MSM both for commitments and as // the subgroup validator for absorbed proof points. - if iszero(staticcall({{ template_constants.eip2537.g1msm_smoke_gas_cap }}, {{ template_constants.eip2537.g1msm_address|hex() }}, scratch, {{ template_constants.g1_msm_pair_bytes|hex() }}, scratch, {{ template_constants.g1_bytes|hex() }})) { revert(0, 0) } + if iszero(staticcall(gas(), {{ template_constants.eip2537.g1msm_address|hex() }}, msm_scratch, {{ constructor_g1msm_smoke_input_bytes|hex() }}, scratch, {{ template_constants.g1_bytes|hex() }})) { revert(0, 0) } if iszero(eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) { revert(0, 0) } if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { revert(0, 0) } - // PAIRING_CHECK([(identity_g1, identity_g2)]) -> true, - // 32-byte return. This catches absent pairing precompiles, + // PAIRING_CHECK([(identity_g1, identity_g2), (identity_g1, identity_g2)]) + // -> true, 32-byte return. This matches the runtime two-pair KZG + // pairing input size and catches absent pairing precompiles, // short return data, and obviously incompatible semantics. - if iszero(staticcall({{ template_constants.eip2537.pairing_smoke_gas_cap }}, {{ template_constants.eip2537.pairing_address|hex() }}, scratch, {{ template_constants.pairing_pair_bytes|hex() }}, scratch, {{ template_constants.word_bytes|hex() }})) { revert(0, 0) } + if iszero(staticcall(gas(), {{ template_constants.eip2537.pairing_address|hex() }}, scratch, {{ template_constants.pairing_two_pair_bytes|hex() }}, scratch, {{ template_constants.word_bytes|hex() }})) { revert(0, 0) } if iszero(eq(returndatasize(), {{ template_constants.word_bytes|hex() }})) { revert(0, 0) } if iszero(eq(mload(scratch), 1)) { revert(0, 0) } } diff --git a/proofs/solidity-verifier/templates/partials/verifier/QuotientAndLinearization.yul b/proofs/solidity-verifier/templates/partials/verifier/QuotientAndLinearization.yul index dfdd1a347..a8d59eb26 100644 --- a/proofs/solidity-verifier/templates/partials/verifier/QuotientAndLinearization.yul +++ b/proofs/solidity-verifier/templates/partials/verifier/QuotientAndLinearization.yul @@ -118,7 +118,7 @@ mstore(add(lin_pair, 0x80), mload(add(SELECTOR_ACC_MPTR, {{ (loop.index0 * 0x20)|hex() }}))) lin_pair := add(lin_pair, 0xa0) {%- endfor %} - let lin_trace_ok := staticcall({{ lin_trace_g1msm_gas_cap }}, {{ template_constants.eip2537.g1msm_address|hex() }}, lin_scratch, {{ ((num_quotients + simple_selector_cols.len()) * template_constants.g1_msm_pair_bytes)|hex() }}, lin_scratch, {{ template_constants.g1_bytes|hex() }}) + let lin_trace_ok := staticcall(gas(), {{ template_constants.eip2537.g1msm_address|hex() }}, lin_scratch, {{ ((num_quotients + simple_selector_cols.len()) * template_constants.g1_msm_pair_bytes)|hex() }}, lin_scratch, {{ template_constants.g1_bytes|hex() }}) lin_trace_ok := and(lin_trace_ok, eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) if iszero(lin_trace_ok) { mstore(TRACE_U256_MPTR, 34) From 1e21311c6e22d11b838bdb23989c0985389696c4 Mon Sep 17 00:00:00 2001 From: Julien Coolen Date: Tue, 19 May 2026 11:04:54 +0100 Subject: [PATCH 12/19] Fail fast on invalid public instances --- docs/audit.md | 10 ++++++++-- proofs/solidity-verifier/src/lowering/tests.rs | 6 ++++++ .../partials/verifier/TranscriptProofParser.yul | 1 + 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/audit.md b/docs/audit.md index ca9aceee8..6b626f448 100644 --- a/docs/audit.md +++ b/docs/audit.md @@ -19,7 +19,7 @@ The main findings are: | M-03 | Medium / High on custom chains | Proof-point validation relies on generated "all absorbed points are later consumed by precompiles" invariant | | M-04 | Medium | Resolved: generated EIP-2537 calls forward `gas()` and constructor smoke covers full-size precompile shapes | | H-INT | High integration risk | Raw verifier does not bind proof meaning to an application, chain, contract, user, nullifier, or program domain | -| L-01 | Low | Invalid public instances are range-checked but failure is deferred until after transcript work | +| L-01 | Low | Resolved: non-canonical public instances now revert immediately after the instance loop | | L-02 | Low | VK header values are not cross-checked against verifier constants after `extcodecopy` | | L-03 | Low | Quotient VM does not assert `q_pc == q_end` after bytecode interpretation | | I-01 | Informational | VK codehash/length should be continuously tested from deployed bytecode | @@ -141,6 +141,9 @@ This is a common audit focus for ZK systems: whether the Fiat-Shamir transcript ### L-01: Invalid public instance range-check failures are deferred +Status: Fixed in the generator. The verifier now reverts immediately after the +public-instance absorption loop if any instance word is not canonical. + During transcript absorption, instance values are checked with: ```solidity @@ -301,7 +304,7 @@ I did a static review of the pasted verifier/VK pair. I did **not** run the gene | 1 | Medium | Point validation / transcript | Proof G1 points are absorbed before curve/subgroup validation; safety depends on every absorbed point later being validated by EIP-2537 on the target chain. | | 2 | Medium | Deployment | Constructor smoke-tests EIP-2537 but not `MCOPY`; deployed verifier can pass constructor and later be unusable on a non-Cancun fork/chain. | | 3 | Low/Medium | Integration | Gas-logging render emits logs and is non-`view`; this can break verifier adapters that use `staticcall` or expect a pure/view verifier. | -| 4 | Low | Gas griefing | Non-canonical public instances are only enforced after the full transcript/proof parsing phase. | +| 4 | Low | Gas griefing | Resolved: non-canonical public instances revert immediately after the instance loop. | | 5 | Low | Generated VM / VK coupling | Quotient VM safety relies on generated bytecode correctness; no runtime bounds checks protect malformed pinned bytecode. | | 6 | Informational | Error handling | Most failures use `revert(0,0)`, making integration and production incident triage unnecessarily hard. | | 7 | Informational | VK contract | VK runtime design is good, but should be CI-checked byte-for-byte against the verifier constants and generator manifest. | @@ -372,6 +375,9 @@ Also include deployment CI against the exact chain/fork config, not only a local **Severity: Low** **Location:** transcript instance absorption +Status: Fixed. The generated transcript parser now checks `success` and reverts +immediately after the instance loop, before reading later proof material. + The instance loop does: ```yul diff --git a/proofs/solidity-verifier/src/lowering/tests.rs b/proofs/solidity-verifier/src/lowering/tests.rs index 01a4eff00..0caf978e4 100644 --- a/proofs/solidity-verifier/src/lowering/tests.rs +++ b/proofs/solidity-verifier/src/lowering/tests.rs @@ -969,6 +969,12 @@ fn failed_success_paths_do_not_enter_ec_precompiles() { verifier_template.contains("success := validate_public_accumulator(success, r)\n if iszero(success) { revert(0, 0) }\n {%- endif %}\n\n {%- if self.gas_checkpoints %}\n gas_checkpoint(2)"), "accumulator precheck should fail before transcript parsing" ); + assert!( + verifier_template.contains( + "success := and(success, lt(inst_be, r))\n // Instances are passed BE in calldata, matching the\n // Keccak Fq transcript input.\n buf_len := common_word(buf_len, inst_be)\n }\n if iszero(success) { revert(0, 0) }" + ), + "non-canonical public instances should fail before proof transcript parsing" + ); assert!( verifier_template.contains( "if iszero(success) { revert(0, 0) }\n\n {%- match quotient_external %}" diff --git a/proofs/solidity-verifier/templates/partials/verifier/TranscriptProofParser.yul b/proofs/solidity-verifier/templates/partials/verifier/TranscriptProofParser.yul index ce80d5964..80e97e1cc 100644 --- a/proofs/solidity-verifier/templates/partials/verifier/TranscriptProofParser.yul +++ b/proofs/solidity-verifier/templates/partials/verifier/TranscriptProofParser.yul @@ -65,6 +65,7 @@ // Keccak Fq transcript input. buf_len := common_word(buf_len, inst_be) } + if iszero(success) { revert(0, 0) } } {%- if self.gas_checkpoints %} From e64f2891036d69125ec5f34e4e3690512f72f46a Mon Sep 17 00:00:00 2001 From: Julien Coolen Date: Tue, 19 May 2026 11:11:05 +0100 Subject: [PATCH 13/19] Cross-check VK header constants --- docs/audit.md | 7 +++++- .../src/lowering/artifacts.rs | 1 + .../src/lowering/render/models.rs | 2 ++ .../solidity-verifier/src/lowering/tests.rs | 25 +++++++++++-------- .../templates/partials/verifier/VkLoading.yul | 15 ++++++++--- 5 files changed, 35 insertions(+), 15 deletions(-) diff --git a/docs/audit.md b/docs/audit.md index 6b626f448..7d5553b56 100644 --- a/docs/audit.md +++ b/docs/audit.md @@ -20,7 +20,7 @@ The main findings are: | M-04 | Medium | Resolved: generated EIP-2537 calls forward `gas()` and constructor smoke covers full-size precompile shapes | | H-INT | High integration risk | Raw verifier does not bind proof meaning to an application, chain, contract, user, nullifier, or program domain | | L-01 | Low | Resolved: non-canonical public instances now revert immediately after the instance loop | -| L-02 | Low | VK header values are not cross-checked against verifier constants after `extcodecopy` | +| L-02 | Low | Resolved: loaded VK header values are cross-checked against verifier constants | | L-03 | Low | Quotient VM does not assert `q_pc == q_end` after bytecode interpretation | | I-01 | Informational | VK codehash/length should be continuously tested from deployed bytecode | | I-02 | Informational | Accumulator encoding logic needs extensive negative tests | @@ -165,6 +165,11 @@ This does not change accepted proofs. ### L-02: VK header values are not cross-checked against verifier constants +**Status:** Resolved. After the VK payload is embedded or copied with +`extcodecopy`, the verifier now cross-checks the generated header words for +instance count, domain `k`, and accumulator layout against the verifier's +compiled-in constants before calldata parsing continues. + The VK payload contains header values: ```text diff --git a/proofs/solidity-verifier/src/lowering/artifacts.rs b/proofs/solidity-verifier/src/lowering/artifacts.rs index 3830f3f51..4dbf611ed 100644 --- a/proofs/solidity-verifier/src/lowering/artifacts.rs +++ b/proofs/solidity-verifier/src/lowering/artifacts.rs @@ -268,6 +268,7 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { fixed_comm_mptr: fixed_comm_mptr_byte, truncated_challenges: cfg!(feature = "truncated-challenges"), expected_has_accumulator, + expected_has_accumulator_word: expected_has_accumulator as usize, expected_acc_offset, expected_num_acc_limbs, expected_num_acc_limb_bits, diff --git a/proofs/solidity-verifier/src/lowering/render/models.rs b/proofs/solidity-verifier/src/lowering/render/models.rs index 532cbeddc..0ff85f228 100644 --- a/proofs/solidity-verifier/src/lowering/render/models.rs +++ b/proofs/solidity-verifier/src/lowering/render/models.rs @@ -516,6 +516,7 @@ pub(crate) struct Halo2Verifier { /// materialization, so a stale or mismatched VK fails before public-input /// decoding chooses the wrong schema. pub(crate) expected_has_accumulator: bool, + pub(crate) expected_has_accumulator_word: usize, pub(crate) expected_acc_offset: usize, pub(crate) expected_num_acc_limbs: usize, pub(crate) expected_num_acc_limb_bits: usize, @@ -1138,6 +1139,7 @@ mod tests { fixed_comm_mptr: 0, truncated_challenges: false, expected_has_accumulator: false, + expected_has_accumulator_word: 0, expected_acc_offset: 0, expected_num_acc_limbs: 0, expected_num_acc_limb_bits: 0, diff --git a/proofs/solidity-verifier/src/lowering/tests.rs b/proofs/solidity-verifier/src/lowering/tests.rs index 0caf978e4..72c7d06bb 100644 --- a/proofs/solidity-verifier/src/lowering/tests.rs +++ b/proofs/solidity-verifier/src/lowering/tests.rs @@ -1148,24 +1148,27 @@ fn accumulator_schema_is_checked_against_instance_count() { } #[test] -fn accumulator_vk_header_is_specialized_at_codegen() { +fn vk_header_is_cross_checked_against_generated_constants() { let verifier_template = verifier_template_corpus(); for expected_check in [ - "eq(mload(HAS_ACCUMULATOR_MPTR)", - "eq(mload(ACC_OFFSET_MPTR)", - "eq(mload(NUM_ACC_LIMBS_MPTR)", - "eq(mload(NUM_ACC_LIMB_BITS_MPTR)", + "eq(mload(NUM_INSTANCES_MPTR), {{ num_instances }})", + "eq(mload(K_MPTR), {{ k }})", + "eq(mload(HAS_ACCUMULATOR_MPTR), {{ expected_has_accumulator_word }})", + "eq(mload(ACC_OFFSET_MPTR), {{ expected_acc_offset }})", + "eq(mload(NUM_ACC_LIMBS_MPTR), {{ expected_num_acc_limbs }})", + "eq(mload(NUM_ACC_LIMB_BITS_MPTR), {{ expected_num_acc_limb_bits }})", ] { assert!( - !verifier_template.contains(expected_check), - "pinned verifier should not reread VK accumulator metadata at runtime: {expected_check}" - ); + verifier_template.contains(expected_check), + "pinned verifier should cross-check generated VK header value: {expected_check}" + ); } assert!( - verifier_template.contains("schema\n // values such as instance count and accumulator layout are\n // rendered as constants"), - "template should document why VK schema values are generated constants" - ); + verifier_template.contains("Cross-check loaded VK header words") + && verifier_template.contains("generator drift before calldata parsing"), + "template should document the VK header generator-safety boundary" + ); assert!( verifier_template.contains("{%- if self.expected_has_accumulator %}") && verifier_template.contains("let bits := {{ self.expected_num_acc_limb_bits }}") diff --git a/proofs/solidity-verifier/templates/partials/verifier/VkLoading.yul b/proofs/solidity-verifier/templates/partials/verifier/VkLoading.yul index 3615efae0..35aca7478 100644 --- a/proofs/solidity-verifier/templates/partials/verifier/VkLoading.yul +++ b/proofs/solidity-verifier/templates/partials/verifier/VkLoading.yul @@ -74,9 +74,18 @@ extcodecopy(vk, VK_MPTR, 0x01, EXPECTED_VK_PAYLOAD_LENGTH) {%- endmatch %} - // This verifier is pinned to one generated VK, so schema - // values such as instance count and accumulator layout are - // rendered as constants instead of reread from the VK header. + // Cross-check loaded VK header words against the verifier + // constants used by later parser, domain, and accumulator + // paths. Codehash pinning protects the external VK address; + // these checks catch generator drift before calldata parsing + // chooses a stale schema. + success := and(success, eq(mload(NUM_INSTANCES_MPTR), {{ num_instances }})) + success := and(success, eq(mload(K_MPTR), {{ k }})) + success := and(success, eq(mload(HAS_ACCUMULATOR_MPTR), {{ expected_has_accumulator_word }})) + success := and(success, eq(mload(ACC_OFFSET_MPTR), {{ expected_acc_offset }})) + success := and(success, eq(mload(NUM_ACC_LIMBS_MPTR), {{ expected_num_acc_limbs }})) + success := and(success, eq(mload(NUM_ACC_LIMB_BITS_MPTR), {{ expected_num_acc_limb_bits }})) + if iszero(success) { revert(0, 0) } // // The checks below validate the dynamic ABI envelope before the // transcript parser starts walking raw calldata: From 33f78d2b306d61ea4aa2487e7790f19600674c6f Mon Sep 17 00:00:00 2001 From: Julien Coolen Date: Tue, 19 May 2026 11:19:14 +0100 Subject: [PATCH 14/19] Assert quotient VM termination --- docs/audit.md | 6 +++++- .../solidity-verifier/src/lowering/tests.rs | 21 +++++++++++++++++++ .../QuotientNumeratorBlock.yul | 6 ++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/audit.md b/docs/audit.md index 7d5553b56..c513f5bc9 100644 --- a/docs/audit.md +++ b/docs/audit.md @@ -21,7 +21,7 @@ The main findings are: | H-INT | High integration risk | Raw verifier does not bind proof meaning to an application, chain, contract, user, nullifier, or program domain | | L-01 | Low | Resolved: non-canonical public instances now revert immediately after the instance loop | | L-02 | Low | Resolved: loaded VK header values are cross-checked against verifier constants | -| L-03 | Low | Quotient VM does not assert `q_pc == q_end` after bytecode interpretation | +| L-03 | Low | Resolved: quotient VM asserts exact bytecode termination and empty final stack | | I-01 | Informational | VK codehash/length should be continuously tested from deployed bytecode | | I-02 | Informational | Accumulator encoding logic needs extensive negative tests | | I-03 | Informational | Terminal absolute-memory Yul strategy is acceptable but brittle | @@ -198,6 +198,10 @@ if iszero(eq(mload(NUM_ACC_LIMB_BITS_MPTR), 56)) { revert(0, 0) } ### L-03: Quotient VM does not assert exact program termination +**Status:** Resolved. The compact quotient VM now reverts unless `q_pc == q_end` +after interpretation and `q_has_top == 0`, and generator tests assert the +planned stack/scratch region covers the validated VM operand-stack bound. + The quotient interpreter stops on: ```solidity diff --git a/proofs/solidity-verifier/src/lowering/tests.rs b/proofs/solidity-verifier/src/lowering/tests.rs index 72c7d06bb..d3ea64048 100644 --- a/proofs/solidity-verifier/src/lowering/tests.rs +++ b/proofs/solidity-verifier/src/lowering/tests.rs @@ -362,6 +362,16 @@ fn lowering_plan_reuses_stable_layout_facts() { assert_eq!(plan.quotient.program.len, plan.quotient.build.bytes.len()); assert!(plan.quotient.build.consts.len() <= plan.vk.quotient_const_words); assert_eq!(plan.quotient.program.stack_mptr, plan.quotient.stack_mptr); + let quotient_stack_region = plan + .memory + .map + .region("quotient_stack") + .expect("quotient stack region should be registered"); + assert_eq!(quotient_stack_region.start, plan.quotient.stack_mptr); + assert!( + quotient_stack_region.len >= plan.quotient.build.max_stack * layout::WORD_BYTES, + "planned quotient stack/scratch region must cover the validated VM operand stack" + ); assert_eq!( plan.quotient.program.eval_numer_mptr, plan.quotient.state_slots.eval_numer_mptr @@ -1545,6 +1555,17 @@ fn quotient_vm_safety_validator_accepts_byte_programs() { assert_eq!(validate_quotient_program(&bytes).unwrap(), 1); } +#[test] +fn quotient_vm_runtime_asserts_exact_program_termination() { + let verifier_template = verifier_template_corpus(); + + assert!( + verifier_template.contains("if iszero(eq(q_pc, q_end)) { revert(0, 0) }") + && verifier_template.contains("if q_has_top { revert(0, 0) }"), + "quotient VM must fail closed when bytecode over-runs q_end or leaves a live stack value" + ); +} + #[test] fn normalized_yul_assignment_parser_tolerates_formatting_variants() { assert_eq!( diff --git a/proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientNumeratorBlock.yul b/proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientNumeratorBlock.yul index 8207e6571..e206aed4e 100644 --- a/proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientNumeratorBlock.yul +++ b/proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientNumeratorBlock.yul @@ -1184,6 +1184,12 @@ revert(0, 0) } } + // The VK-pinned bytecode must end exactly at q_end and every + // identity must have been consumed by a fold/native callback. + // This catches malformed generator output whose final opcode + // over-reads operands or leaves a partial expression live. + if iszero(eq(q_pc, q_end)) { revert(0, 0) } + if q_has_top { revert(0, 0) } // Structured post-VM suffix. The current default uses this for // regular trash constraints: it is smaller than fully unrolled From 780af85b7bc0a05f30104fc1a2860336c213da15 Mon Sep 17 00:00:00 2001 From: Julien Coolen Date: Tue, 19 May 2026 11:43:51 +0100 Subject: [PATCH 15/19] Update Moonlight Sepolia verifier deployment --- .../Halo2Verifier.runtime.bytecode.hex | 2 +- .../sepolia/moonlight-wrap/Halo2Verifier.sol | 150 ++++++++++++++---- .../sepolia/moonlight-wrap/README.md | 20 ++- .../sepolia/moonlight-wrap/deployment.json | 18 +-- 4 files changed, 140 insertions(+), 50 deletions(-) diff --git a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.runtime.bytecode.hex b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.runtime.bytecode.hex index 9cc24914b..2ba2ddc38 100644 --- a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.runtime.bytecode.hex +++ b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.runtime.bytecode.hex @@ -1 +1 @@ -0x6108e06040526004361015610012575f80fd5b5f3560e01c80631e8e1e1314610078576342b7259714610030575f80fd5b34610074575f366003190112610074576040517f0000000000000000000000002fe3db07cc00f6dff002c37d5a17ebfad7aa88d06001600160a01b03168152602090f35b5f80fd5b346100745760403660031901126100745760043567ffffffffffffffff8111610074573660238201121561007457806004013567ffffffffffffffff81116100745760243691830101116100745760243567ffffffffffffffff8111610074573660238201121561007457806004013567ffffffffffffffff8111610074576024369160051b8301011161007457611ec060409114911416156100745760017f0000000000000000000000002fe3db07cc00f6dff002c37d5a17ebfad7aa88d0803b61428114813f7f24430c63ec2017f12ec4004d66c66314f418dd77fe38dd4c2475c8f2b259e0091416156100745781612700614280923c6121443614611ec435601314604435611e6014831616168015610074575f90731a0111ea397fe69a4b1ba7b6434bacd764774b846120a435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa612084351416731a0111ea397fe69a4b1ba7b6434bacd764774b8461206435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa6120443514161680614c43575b15614baf575b166080616bc061a1c05e808261a24052614b94575b5f90731a0111ea397fe69a4b1ba7b6434bacd764774b8461212435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa612104351416731a0111ea397fe69a4b1ba7b6434bacd764774b846120e435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa6120c43514161680614b77575b15614ae3575b1680916080616c4061a1c05e61a24052614ac8575b80156100745760806127005190525f60a0525f60c0525f60e0525f61010052601361012052610140611ee45b6121448110614aa457506064906191c0905b826107e48110614a87575f5160206151df5f395f51905f528592607f190160802080608052066169805260a090619940916101008201925b838310614a695784835f5160206151df5f395f51905f5284607f190160802080608052066169a0525f5160206151df5f395f51905f52602060802080608052066169c05260a0619a4061030083015b808410614a47575050619d40608083015b808410614a25575061010060806103b8858095614d69565b948181619e4037019201905b818310614a035750505f5160206151df5f395f51905f5260806103e8838095614d69565b928181619ec0370191607f190160802080608052066169e05260a0610100619f409301925b8383106149e55784835f5160206151df5f395f51905f5284607f19016080208060805206616a005260a090619fc0916102008201925b8383106149c75784835f5160206151df5f395f51905f5284607f19016080208060805206616a205260a090618500610cc08201905b8183106149965750505f5160206151df5f395f51905f528192607f19016080208060805206616a40525f5160206151df5f395f51905f5260206080208060805206616a605261012060806104cb83614cd3565b928181616ac0370191607f190160802092836080526001600160801b035f5160206151df5f395f51905f5260a0950616616a8052826182a052015b80821061496e57506080905f5160206151df5f395f51905f52611ec493607f1901832080845206616aa05261053a81614cd3565b508181616b4037010361007457801561007457616a205180915f5b6014811061495357506127805192616cc0846127c0515b617060831061492e57505050506105a55f5160206151df5f395f51905f525f51602061523f5f395f51905f528408918261706052614dfd565b925f5160206151df5f395f51905f52616cc092612760519009916127c0515b6170608210614909578585616ce05190616d00915b616e0083106148ec575f92611ee4905b61214482106148c757505061706051616cc05190616e005193616cc052616ce052616d0052616d2052616d4052616d6052801561007457616a0051610300525f61a300525f5b61014081106148b857506001805b6031821061488f5782618b4051618a40516185205190618a6051916185405161088052618a8051936185605194618aa0516108c0526185805190618ac0516185a0516108a052618ae05191886185c0519586946108805189618b0051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f529109955f5160206151df5f395f51905f529109936108a0515f5160206151df5f395f51905f52910992866108c051905f5160206151df5f395f51905f529109925f5160206151df5f395f51905f52910990610880515f5160206151df5f395f51905f52908c09905f5160206151df5f395f51905f528a8c095f5160206151df5f395f51905f529108905f5160206151df5f395f51905f529108905f5160206151df5f395f51905f529108905f5160206151df5f395f51905f529108905f5160206151df5f395f51905f529108905f5160206151df5f395f51905f529108905f5160206151df5f395f51905f5291088684618b2051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f5291085f5160206151df5f395f51905f5290600109956103005161a30051905f5160206151df5f395f51905f5291099661a1c051905f5160206151df5f395f51905f52910861a1c0526108a0515f5160206151df5f395f51905f52035f5160206151df5f395f51905f52905f08915f5160206151df5f395f51905f52035f5160206151df5f395f51905f52905f089061088051905f5160206151df5f395f51905f529108905f5160206151df5f395f51905f529108905f5160206151df5f395f51905f5291085f5160206151df5f395f51905f529060010961a1e051905f5160206151df5f395f51905f52910861a1e0525f5160206151df5f395f51905f52035f5160206151df5f395f51905f52905f08915f5160206151df5f395f51905f529108905f5160206151df5f395f51905f5291085f5160206151df5f395f51905f529060010961a20051905f5160206151df5f395f51905f529108906185e0515f5160206151df5f395f51905f52035f5160206151df5f395f51905f52905f089061088051905f5160206151df5f395f51905f529108905f5160206151df5f395f51905f5291085f5160206151df5f395f51905f5290600109918261a9605261030051906103005190610300515f5160206151df5f395f51905f529109905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f52910961a3005261a360515f5160206151df5f395f51905f529109905f5160206151df5f395f51905f52910861a20052614120905f9061a960825b84906152ef861015611ece57600186515f1a9601959182600514611ea2575081600614611e825781600814611e675781600d14611e3d5781601014611e195781601114611df55781602114611b5657508060191461180b5780601f146112b55780601b14610af657600b14610a85575f80fd5b600384519401935f905f5160206151df5f395f51905f526103005161a300510961a300525f5160206151df5f395f51905f5285611fe08360f31c1661a1c0019283519061ffff8160e81c16610ade575b50089052610a12565b90621fffe0849260e31c1661a3400151900989610ad5565b505082516002909301925f925061a96090839060f01c8015610e225780600114610cf65780600214610c1157600314610b2d575f80fd5b5f5160206151df5f395f51905f528080808080618b0051816185e05181035f0890088180618520518161858051918009097f404d21073985d14e432a4ad76d3fae06ca74314b950fe7b1d7f501cd31a8b374099008818061854051816185a051918009097f0b2cc8704264c6bd81bc620e9e524d4b73e9b2317679422ff7fa1603955649f10990088180618560518161862051918009097f0fdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f099008600109808452816103005161a300510961a300528161a2e05161a3605190090861a2e052610a12565b505f5160206151df5f395f51905f528080808080618ae051816185c05181035f0890088180618520518161858051918009097f1b8114c381b922fd5d6d241210e2d8a68ad5744053ba9e776118de4107b51ace099008818061854051816185a051918009097f3df32e4cc4cb2ed20e5d21899cf5331775990ccaec4c09b4e3717213fcc0d7630990088180618560518161862051918009097f3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc1099008600109808452816103005161a300510961a300528161a2e05161a3605190090861a2e052610a12565b505f5160206151df5f395f51905f528080807f73eda753299d7d483339d80809a1d7f7b67900f7fe6bfad98998021b7bb5732b81807f73eda753299d7d483339d80809a1d80553bca402fffe5bfeffffffff0000000181808080600160701b61852051088180600160701b6185405108600160381b0990088180600160701b6185605108600160701b099008818080618660518161868051600160381b0990086186a0518290600160701b09900881035f08900808818070241eabfffeb153ffffb9feffffffffaaab6187c0510981035f089008088180600160861b817f73eda753299d7d483339d80809a1d80553bda402fffe5b6e855000003ab000026187e051080981035f089008600109808452816103005161a300510961a3005261a240510861a24052610a12565b5061852051610400526185c051610520526185e051610480526186005161044052618540516104a0526187a0516104c0525f5160206151df5f395f51905f528080807f73eda753299d7d483339d80809a1d7d340dd972492de594de627fffefcb803498180618560516187805161054052618580516105805261876051610460526185a0516104205261874051610500526186205161056052618640516104e05281808080618660518161868051600160381b0990086186a0518290600160701b09900881035f089181808061044051600160701b09818061048051600160381b096105205108089181808083600160701b0981806104a051600160381b09610400510808918180806104c0516104e05109700c8557e86f90d0d89eed6eb5349a0f88200991818080610540516104e05109702cb9b546d20373eaf85e8f53db883cb5480991818080610460516104e0510970013af65741744bd7bb2c6872df2b8003200991818080610500516104e0510970340f2ebe380a0f5eff4360543988a61dc20991818080610440516104e0510970297784894e27525bc342b7fde37dba93660991818080610480516104e05109703212e00cde6d2002b119d800000347fcb809918180806104c0516105605109702cb9b546d20373eaf85e8f53db883cb548099181808061054051610560510970013af65741744bd7bb2c6872df2b800320099181808061046051610560510970340f2ebe380a0f5eff4360543988a61dc2099181808061050051610560510970297784894e27525bc342b7fde37dba93660991818080610440516105605109703212e00cde6d2002b119d800000347fcb809918180806104c051610420510970013af65741744bd7bb2c6872df2b800320099181808061054051610420510970340f2ebe380a0f5eff4360543988a61dc2099181808061046051610420510970297784894e27525bc342b7fde37dba93660991818080610500516104205109703212e00cde6d2002b119d800000347fcb809918180806104c051610580510970340f2ebe380a0f5eff4360543988a61dc2099181808061054051610580510970297784894e27525bc342b7fde37dba93660991818080610460516105805109703212e00cde6d2002b119d800000347fcb809918180806104c051840970297784894e27525bc342b7fde37dba936609918180808080610540518609703212e00cde6d2002b119d800000347fcb80993610520519009600160701b098180806104c0516104a05109703212e00cde6d2002b119d800000347fcb809818080610480516104a05109600160701b09818080610520516104a05109600160381b09818080610440516104005109600160701b09818080610480516104005109600160381b09816105205161040051090808080808080808080808080808080808080808080808080808080808818070241eabfffeb153ffffb9feffffffffaaab6187c0510981035f089008088180600160861b817f73eda753299d7d483339d80809a1d80553b9202d7ffe85d4800008bb200000016187e051080981035f089008600109808452816103005161a300510961a3005261a220510861a22052610a12565b505090505f905f61a96090616d40516103a052616d005161032052616d20516103e0526169a05161038052616980516103c0525f5160206151df5f395f51905f52806190e05181610320516103a0510809816103005161a30051090861a300525f5160206151df5f395f51905f52618b6051816103c05191816103c0515f0908095f5b600481106117de57505060015f5b600481106117bc5750600161a9e05260015b6004811061178a5750600161aac0526003805b61175857505f905f5b6004811061172b5750905f5160206151df5f395f51905f52808080808096816190c05197810391880908816103005161a3005109089381808080618be051948180618b8051816103c0515f090881618ba051916103c05190090895096190e0510881036191005108816190a0519361038051900890090881806103e05161032051088103600108099161030051900908610360526191605161034052618a00516102e05261852051610260526185405161028052618560516102c052618580516102a0526185a0516102405261862051610220526186405161020052618660516101e052618680516101c0526186a0516101a0526186c051610180526186e0516101605261870051610140526187205161012052618bc051610100526191405160e0525f5160206151df5f395f51905f5280618c005181035f0860010860c0525f5160206151df5f395f51905f52808060e05160010961034051088103619180510860a0525f5160206151df5f395f51905f5280806191205181806103805181806101005160c05109816103c05181806101205160c05109816103c05181806101405160c05109816103c05181806101605160c05109816103c05181806101805160c05109816103c05181806101a05160c05109816103c05181806101c05160c05109816103c05181806101e05160c05109816103c05181806102005160c05109816103c05181806102205160c05109816103c05181806102405160c05109816103c05181806102a05160c05109816103c05181806102c05160c05109816103c05181806102805160c05109816103c05181806102605160c05109816103c05181806102e05160c05109816103c0515f09080908090809080908090809080908090809080908090809080908090809080860a051090881806103e0516103205108810360010809816103005181805f51602061523f5f395f51905f528180610380518161010051816103c0518161012051816103c0518161014051816103c0518161016051816103c0518161018051816103c051816101a051816103c051816101c051816103c051816101e051816103c0518161020051816103c0518161022051816103c0518161024051816103c051816102a051816103c051816102c051816103c0518161028051816103c0518161026051816103c051816102e051816103c0515f09080908090809080908090809080908090809080908090809080908090809080860e0510908816103005181806103405181610320516103a051080981610300516103605109080908090861a30052610a12565b915f5160206151df5f395f51905f52600191818560051b8061aa6001519061a9e001510990089201611374565b5f5160206151df5f395f51905f528160051b808601519061aa600151095f19820160051b61aa6001525f19018061136b565b6001905f5160206151df5f395f51905f525f19820160051b808701519061a9e00151098160051b61a9e0015201611358565b905f5160206151df5f395f51905f526001918360051b86015190099101611346565b8060019160051b5f5160206151df5f395f51905f5261038051818361854001518708089086015201611338565b5050618a205161a9609081525f925082805b60058110611b3d57506185005161aa2052616d605161aa40525f5b60098110611b245750618a005161ab80525f5b60128110611b0b57505f5b60068110611af057505f5b60068110611ad557505f5b60058110611aba57505f5160206151df5f395f51905f5280808061ade0518103600108616d405109816103005161a30051090881808061ae80518181810391800908616d005109916103005190090861a3005260015b60068110611a7457505f5160206151df5f395f51905f52616a20516169a0510961b000525f5b600681106118f65750610a12565b600381026003810160128111611a6c575b8260051b908161aea001519161ade001519061b00051908194906169c051906169a0515b8a82851061199f57505050505050600193925f5160206151df5f395f51905f52808080957f4285088329c399ea457a8ca1d30f8957e74c7f529842a1579b4fee55b398292395820390088180616d2051616d0051088103890809816103005161a30051090861a300520961b00052016118e8565b9285979385969792939482809760051b809301519261aba001515f5160206151df5f395f51905f529087095f5160206151df5f395f51905f52908408905f5160206151df5f395f51905f5291085f5160206151df5f395f51905f529109985f5160206151df5f395f51905f529108905f5160206151df5f395f51905f5291085f5160206151df5f395f51905f529109947f08634d0aa021aaf843cab354fabb0062f6502437c6a09c006c083479590189d75f5160206151df5f395f51905f5291099360010192919061192b565b506012611907565b805f5160206151df5f395f51905f52808060019460051b61ade001515f19850160051b61af60015182039008616d405109816103005161a30051090861a30052016118c2565b80606060019202618ec001518160051b61af6001520161186c565b80606060019202618ea001518160051b61aea0015201611861565b80606060019202618e8001518160051b61ade0015201611856565b8060019160051b80618c4001519061aba001520161184b565b8060019160051b8061862001519061aa60015201611838565b8060019160051b8061852001519061a98001520161181d565b92939480915090600181515f1a91015f9260018316611de5575b505f9360028316611dcc575b815196875f1a8860011a908960021a9a60058b60041a960199611dbe575b505f5b818110611d715750505f5b818110611d125750505f5b878a821015611c4a5788519860118a60f01c9101995f5b60078110611bde5750505050600101611bb3565b8060051b8301515f6080525b600760805110611bfd5750600101611bca565b9a5f5160206151df5f395f51905f5290818d81600460805187018a0101515f1a60051b612ae001519160805160051b61ffff8960e01c16015190090990089a600160805101608052611bea565b5050959750955f905b8060031a8210611cd85750505f905b808210611c94575050600116611c7c575b50916001610a12565b905f5160206151df5f395f51905f5291510984611c73565b90935f5160206151df5f395f51905f526001918160058b519b019a81815f1a60051b612ae001519161ffff808260d81c16519160e81c165109099008940190611c62565b90945f5160206151df5f395f51905f526001918160038c519c019b61ffff8160e81c1651905f1a60051b612ae00151099008950190611c53565b6002895160f01c990198515f905b8a60078310611d3457505050600101611ba8565b600191929a5f5160206151df5f395f51905f5260038193519e019d8161ffff825f1a60051b612ae001519260e81c16518709099008990190611d20565b5f5b8a60078210611d86575050600101611b9d565b6001919a5f5160206151df5f395f51905f5260038193519e019d61ffff8160e81c1651905f1a60051b612ae001510990089901611d73565b84526020909301928b611b9a565b9350600181515f1a91019060051b612ae0015193611b7c565b905160f01c925060030187611b70565b935f5160206151df5f395f51905f5291506002865160f01c96019551900992610a12565b935f5160206151df5f395f51905f5291506002865160f01c96019551900892610a12565b935f5160206151df5f395f51905f529150600186515f1a96019560051b612ae00151900992610a12565b935f5160206151df5f395f51905f52809250035f0892610a12565b93915f5160206151df5f395f51905f529150601f19019182510892610a12565b92949150946003905160f01c920194611ec0575b5051916001610a12565b835260209092019184611eb6565b836169e051610840525f5160206151df5f395f51905f52618ae051815f51602061523f5f395f51905f526185c051099008610720526185205161082052618540516108005261856051610760525f5160206151df5f395f51905f526107605161076051096107e05261858051610860525f5160206151df5f395f51905f52610860516108605109610740526185a0516107a0525f5160206151df5f395f51905f526107a0516107a051096107805261862051610700525f5160206151df5f395f51905f526107005161070051096107c052618640516106e0525f5160206151df5f395f51905f526106e0516106e051096106c052618660516106a0525f5160206151df5f395f51905f526106a0516106a05109610680525f5160206151df5f395f51905f52618b0051815f51602061523f5f395f51905f526185e05109900861066052618b205161064052618b405161062052618a405161060052618a60516105e052618a80516105c0525f5160206151df5f395f51905f52618aa051815f51602061523f5f395f51905f52618600510990086105a0525f5160206151df5f395f51905f5280808080618c205181036001086191a0519009810381808080806106805161068051096106a051095f5160206151ff5f395f51905f5209818080806106c0516106c051096106e051095f51602061525f5f395f51905f5209818080806107c0516107c0510961070051095f5160206151bf5f395f51905f5209818080806107805161078051096107a051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e098180808061074051610740510961086051097ed94c46a8456352aa44d7a885ab59e3a36664e6fb25e826f8a4cd79822f053309818080806107e0516107e0510961076051097f1f61345b652161410c5e29f51e301ae56342af824bc110649393d2b911c50d3e098180610800517f40fa389feb2522bb934881ac9ed749aee2296502af592418c6b5675c0f560261098180610820517f70d8f2a733a64d650faccc9b1c2a766a9544bb3ff1a11ee73cb43947ef386633096105a0510808080808080808816108405181808080806106c0516106c051096106e051095f5160206151ff5f395f51905f5209818080806107c0516107c0510961070051095f51602061525f5f395f51905f5209818080806107805161078051096107a051095f5160206151bf5f395f51905f52098180808061074051610740510961086051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e09818080806107e0516107e0510961076051097ed94c46a8456352aa44d7a885ab59e3a36664e6fb25e826f8a4cd79822f0533098180610800517f27e7119226c42a6d19c1541904b99ae40685511ed2e078964b74594d38340849098180610820517f6d05a41959f539a7fc9ec0972ea1e3dbb6fc67dd51daf3414f7fbbb091c7274a0981805f51602061523f5f395f51905f526106a051096105c0510808080808080808816108405181808080806107c0516107c0510961070051095f5160206151ff5f395f51905f5209818080806107805161078051096107a051095f51602061525f5f395f51905f52098180808061074051610740510961086051095f5160206151bf5f395f51905f5209818080806107e0516107e0510961076051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e098180610800517f23a6684b942d726a22e4d5b8d8ff83aeaa773f62600184efe5d033d7c7c6e827098180610820517f2f5908b169c6cf1bd26dcf0f9e5105481f5164f3ece0582bf3098312167751a70981805f51602061523f5f395f51905f526106e051096105e05108080808080808816108405181808080806107805161078051096107a051095f5160206151ff5f395f51905f52098180808061074051610740510961086051095f51602061525f5f395f51905f5209818080806107e0516107e0510961076051095f5160206151bf5f395f51905f52098180610800517f24822e1af9aa2887c912c87eb0f20bd332330e7e55cd784de67cb407a9f05520098180610820517f726df1506749848155630b86ae25a82b281ecd050fe3a52d85a181fa87202e4b0981805f51602061523f5f395f51905f526107005109610600510808080808088161084051818080808061074051610740510961086051095f5160206151ff5f395f51905f5209818080806107e0516107e0510961076051095f51602061525f5f395f51905f52098180610800517f26c2cc87f95726b28f33ca03409a460ec987cfe12adae32769e3565865d07191098180610820517f222e83e70453dfee19b402e9fa8dfe2c4987b034d0be3ceb478b3022e97934c10981805f51602061523f5f395f51905f526107a05109610620510808080808816108405181808080806107e0516107e0510961076051095f5160206151ff5f395f51905f52098180610800517f6bd72f9cfc53af9d931896e77ea5c61244cb6d5fae8954f37dc7b9002f5aa78a098180610820517f5e1d3dbecda6214343e24a47f45c5d033197ad01b65a730af95dc57e90c491400981805f51602061523f5f395f51905f5261086051096106405108080808816108405181808080806106805161068051096106a051097f0fdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f09818080806106c0516106c051096106e051097f301cf56f9b4577112cc4241cddf6484aaadedbf1bbd0f2351adf2e41c2fb2ecd09818080806107c0516107c0510961070051097f5f3a15bab4ce4097b1edc3a25002694b92395ce355a8a12fe557459d9633f70109818080806107805161078051096107a051097f275a20361ea91992193920270d3e2d1f6361880ac0a439c64bef815d4469ba85098180808061074051610740510961086051097f31e823a45e567484c1544e310c0fa5cd66547a8f0dde659ac61698c30e838d2509818080806107e0516107e0510961076051097f26cc223e16f47c20e17cc6069605fa5a8af05ea4f6eb36029a641d23b818eb10098180610800517f4d0ea7f9c3fda06d9535b0fdafd8338bd47c2200b284fa71a325ff41ac358028098180610820517f5b1fc262a28cbb8bf75d9b1a6edaa74591ec24cd9a209512213cec3a3c0f1a5d09610660510808080808080808816108405181808080806106805161068051096106a051097f3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc109818080806106c0516106c051096106e051097f06ccb1c7d87f3c12a2bde4e68ac7f1e8b03481ba15d7f88f9a7f9b8310dd6d3409818080806107c0516107c0510961070051097f53fded36d490ba6b05a5d10fd99ffe5456baec6a6a8753199d5ebdc33c99790e09818080806107805161078051096107a051097f412c98232b6ab8a47aa76ee814ef7ec6261987c9802f2cfc490e007951a60ca5098180808061074051610740510961086051097f333f8046ece5579cbd6872449c57f2703dfc8864cfadc06d587ff104a0d0c1f209818080806107e0516107e0510961076051097f3509dd2fe3aac0080783557fec090fb1cb4b2b0901253c55282024331d1fe1a8098180610800517f52f789e4afc3801f7411102ee2f47cc5954a744e71cac98e75ea962a55a0a76f098180610820517f590ba402032e82eb1f660ef09796c5686345a5054ed96dae8e2d2336337887710961072051080808080808080881610840515f0908090809080908090809080908090808816103005161a3005109088161a9405161a1c0510961a1c0528161a9205161a1e0510961a1e0528161a8c05161a200510961a200528161a8605161a220510961a220528161a8005161a240510961a240528161a7a05161a260510961a260528161a7405161a280510961a280528161a6e05161a2a0510961a2a0528161a6805161a2c0510961a2c0528161a5c05161a2e0510961a2e05281035f08616d8052616a2051908160015f5b6014811061486c57505f5160206151df5f395f51905f5280808080888180988198616da0528103600108616dc05281808061278051816127a051809c81809c81809c818c819d9b829c9a839b6170405282096170605209806170205209090909090909090961700052616a4051600161738052600190617380905f915b602a83106148405784618a0061a3005261850061a320526190a061a340526190c061a3605261912061a3805261914061a3a0526191a061a3c052618a2061a3e052618a4061a40052618a6061a42052618a8061a44052618aa061a46052618ac061a48052618ae061a4a052618b0061a4c052618b2061a4e052618b4061a50052618b6061a52052618b8061a54052618ba061a56052618bc061a58052618be061a5a052618c0061a5c052618c2061a5e052618c4061a60052618c6061a62052618c8061a64052618ca061a66052618cc061a68052618ce061a6a052618d0061a6c052618d2061a6e052618d4061a70052618d6061a72052618d8061a74052618da061a76052618dc061a78052618de061a7a052618e0061a7c052618e2061a7e052618e4061a80052618e6061a82052616d8061a84052618a00516173a061a32060015b602b811061481557505050617ba0526186e0515f5160206151df5f395f51905f526189a0519181806173a051928184618700510990089381836189c05109900882806173c051958187618720510990089181866189e05109900890617bc052617be052818080619060518180619080519281886190e051099008956191005109900892818661916051099008936191805109900890617c0052617c205261852061a300526185c061a3205261884061a3405261854061a360526185e061a3805261886061a3a05261856061a3c05261860061a3e05261888061a4005261858061a4205261874061a440526188a061a460526185a061a4805261876061a4a0526188c061a4c05261862061a4e05261878061a500526188e061a5205261864061a540526187a061a5605261890061a5805261866061a5a0526187c061a5c05261892061a5e05261868061a600526187e061a6205261894061a640526186a061a6605261880061a6805261896061a6a0526186c061a6c05261882061a6e05261898061a70052618520516185c05161884051916173a061a36060015b600b81106147cc57505050617c4052617c6052617c8052618e8061a30052618ea061a32052618ec061a34052618ee061a36052618f0061a38052618f2061a3a052618f4061a3c052618f6061a3e052618f8061a40052618fa061a42052618fc061a44052618fe061a4605261900061a4805261902061a4a05261904061a4c052618e8051618ea051618ec051916173a061a36060015b6005811061478357505050617ca052617cc052617ce052616a6051616a80516182a0516170005191836170205181617040519581617060519188815f5160206151df5f395f51905f52035f5160206151df5f395f51905f529089085f5160206151df5f395f51905f528581038a085f5160206151df5f395f51905f528381038b08905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f529109915f5160206151df5f395f51905f5281810383085f5160206151df5f395f51905f5286810384085f5160206151df5f395f51905f5290600109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52908409895f5160206151df5f395f51905f5283810388085f5160206151df5f395f51905f5285810389085f5160206151df5f395f51905f5290600109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f5290830994878d5f5160206151df5f395f51905f5282810387085f5160206151df5f395f51905f5288810388085f5160206151df5f395f51905f5290600109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52908909612ff690614c60565b955f5160206151df5f395f51905f5283810382085f5160206151df5f395f51905f5289810383085f5160206151df5f395f51905f5290600109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52908809935f5160206151df5f395f51905f5282810385085f5160206151df5f395f51905f528a810386085f5160206151df5f395f51905f5290600109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f529086095f5160206151df5f395f51905f528381038b085f5160206151df5f395f51905f528681038c085f5160206151df5f395f51905f5290600109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52908209995f5160206151df5f395f51905f5286810389085f5160206151df5f395f51905f528281038a08905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52908c099a8b945f5160206151df5f395f51905f52035f5160206151df5f395f51905f52908a085f5160206151df5f395f51905f529109905f5160206151df5f395f51905f52035f5160206151df5f395f51905f529089085f5160206151df5f395f51905f529082099788965f5160206151df5f395f51905f52035f5160206151df5f395f51905f5291085f5160206151df5f395f51905f529109915f5160206151df5f395f51905f52910981617ca051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f5203935f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52916080013509905f5160206151df5f395f51905f529108925f5160206151df5f395f51905f52910990617cc051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52035f5160206151df5f395f51905f529108925f5160206151df5f395f51905f52910990617ce051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52035f5160206151df5f395f51905f5291085f5160206151df5f395f51905f52825f09905f5160206151df5f395f51905f52910887895f5160206151df5f395f51905f5285810388085f5160206151df5f395f51905f5282810389085f5160206151df5f395f51905f5290600109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f528881038b085f5160206151df5f395f51905f528781038c085f5160206151df5f395f51905f528481038d08905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291098a5f5160206151df5f395f51905f528a810385085f5160206151df5f395f51905f5289810386085f5160206151df5f395f51905f5290600109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f5290830991885f5160206151df5f395f51905f528c810382085f5160206151df5f395f51905f5287810383085f5160206151df5f395f51905f5290600109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52908509968c5f5160206151df5f395f51905f52878a096134bd90614c60565b965f5160206151df5f395f51905f52908809935f5160206151df5f395f51905f5282810385085f5160206151df5f395f51905f528a810386085f5160206151df5f395f51905f5290600109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f529086095f5160206151df5f395f51905f528381038b085f5160206151df5f395f51905f528681038c085f5160206151df5f395f51905f5290600109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52908209995f5160206151df5f395f51905f5286810389085f5160206151df5f395f51905f528281038a08905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52908c099a8b945f5160206151df5f395f51905f52035f5160206151df5f395f51905f52908a085f5160206151df5f395f51905f529109905f5160206151df5f395f51905f52035f5160206151df5f395f51905f529089085f5160206151df5f395f51905f529082099788965f5160206151df5f395f51905f52035f5160206151df5f395f51905f5291085f5160206151df5f395f51905f529109915f5160206151df5f395f51905f52910981617c4051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f5203935f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52916060013509905f5160206151df5f395f51905f529108925f5160206151df5f395f51905f52910990617c6051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52035f5160206151df5f395f51905f529108925f5160206151df5f395f51905f52910990617c8051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52035f5160206151df5f395f51905f529108915f5160206151df5f395f51905f529109905f5160206151df5f395f51905f529108905f5160206151df5f395f51905f5281810389085f5160206151df5f395f51905f52906001095f5160206151df5f395f51905f5282810388085f5160206151df5f395f51905f528a81038908905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5289810382085f5160206151df5f395f51905f52906001095f5160206151df5f395f51905f5290830961387090614c60565b5f5160206151df5f395f51905f528a810383085f5160206151df5f395f51905f52906001095f5160206151df5f395f51905f52908209915f5160206151df5f395f51905f528181038c085f5160206151df5f395f51905f52906001095f5160206151df5f395f51905f52908409905f5160206151df5f395f51905f528c81038b085f5160206151df5f395f51905f529083099384925f5160206151df5f395f51905f528381038d085f5160206151df5f395f51905f529109915f5160206151df5f395f51905f52035f5160206151df5f395f51905f52908c085f5160206151df5f395f51905f528e81038d08905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52910981617c0051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f5203915f5160206151df5f395f51905f5291095f5160206151df5f395f51905f529060408c013509905f5160206151df5f395f51905f529108925f5160206151df5f395f51905f52910990617c2051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52035f5160206151df5f395f51905f529108915f5160206151df5f395f51905f529109905f5160206151df5f395f51905f529108905f5160206151df5f395f51905f5281810387085f5160206151df5f395f51905f52906001095f5160206151df5f395f51905f5282810386085f5160206151df5f395f51905f528881038708905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52828209925f5160206151df5f395f51905f5289810382085f5160206151df5f395f51905f52906001095f5160206151df5f395f51905f52908509613b1190614c60565b915f5160206151df5f395f51905f528a810383085f5160206151df5f395f51905f52906001095f5160206151df5f395f51905f52908409935f5160206151df5f395f51905f52908509935f5160206151df5f395f51905f528b81038a085f5160206151df5f395f51905f529086099485935f5160206151df5f395f51905f52035f5160206151df5f395f51905f52908b085f5160206151df5f395f51905f529109915f5160206151df5f395f51905f52910981617bc051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f5203915f5160206151df5f395f51905f5291095f5160206151df5f395f51905f529060208a013509905f5160206151df5f395f51905f529108925f5160206151df5f395f51905f52910990617be051905f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291095f5160206151df5f395f51905f52035f5160206151df5f395f51905f529108915f5160206151df5f395f51905f529109905f5160206151df5f395f51905f529108925f5160206151df5f395f51905f52035f5160206151df5f395f51905f529108613cd290614c60565b90617ba0515f5160206151df5f395f51905f52039035905f5160206151df5f395f51905f529108905f5160206151df5f395f51905f529109915f5160206151df5f395f51905f529109905f5160206151df5f395f51905f5291089081616e4052616aa05180808094616da051616dc051916182a051925f5160206151df5f395f51905f52856001096001600160801b038116955f5160206151df5f395f51905f5291096001600160801b038116965f5160206151df5f395f51905f5291096001600160801b038116995f5160206151df5f395f51905f5291096001600160801b038116975f5160206151df5f395f51905f5291096001600160801b03169260806198c061a3005e600161a38052608061994061a3a05e6173c05161a420526080619d4061a4405e6173e05161a4c05260806199c061a4e05e6174005161a560526080619dc061a5805e6174205161a600526080619f4061a6205e6174405161a6a052608061578061a6c05e6174605161a74052608061550061a7605e6174805161a7e052608061558061a8005e6174a05161a88052608061560061a8a05e6174c05161a92052608061568061a9405e6174e05161a9c052608061570061a9e05e6175005161aa6052608061530061aa805e6175205161ab0052608061538061ab205e6175405161aba052608061540061abc05e6175605161ac4052608061548061ac605e6175805161ace052608061580061ad005e6175a05161ad8052608061588061ada05e6175c05161ae2052608061590061ae405e6175e05161aec052608061598061aee05e6176005161af60526080615b8061af805e6176205161b000526080615f0061b0205e6176405161b0a052608061600061b0c05e6176605161b14052608061608061b1605e6176805161b1e052608061610061b2005e6176a05161b28052608061618061b2a05e6176c05161b32052608061620061b3405e6176e05161b3c052608061628061b3e05e6177005161b46052608061630061b4805e6177205161b50052608061638061b5205e6177405161b5a052608061640061b5c05e6177605161b64052608061648061b6605e6177805161b6e052608061650061b7005e6177a05161b78052608061658061b7a05e6177c05161b82052608061660061b8405e6177e05161b8c052608061668061b8e05e6178005161b96052608061670061b9805e6178205161ba0052608061678061ba205e6178405161baa052608061680061bac05e6178605161bb4052608061688061bb605e6178805161bbe052608061690061bc005e6178a05161bc80526178c051915f5160206151df5f395f51905f529083096080619fc061bca05e818161bd20525f5160206151df5f395f51905f529109608061a04061bd405e818161bdc0525f5160206151df5f395f51905f52910990608061a0c061bde05e8161be6052608061a14061be805e5f5160206151df5f395f51905f52910961bf00526080615a0061bf205e61a1c0515f5160206151df5f395f51905f5290820961bfa0526080615a8061bfc05e61a1e0515f5160206151df5f395f51905f5290820961c040526080615b0061c0605e61a200515f5160206151df5f395f51905f5290820961c0e0526080615c0061c1005e61a220515f5160206151df5f395f51905f5290820961c180526080615c8061c1a05e61a240515f5160206151df5f395f51905f5290820961c220526080615d0061c2405e61a260515f5160206151df5f395f51905f5290820961c2c0526080615d8061c2e05e61a280515f5160206151df5f395f51905f5290820961c360526080615e0061c3805e61a2a0515f5160206151df5f395f51905f5290820961c400526080615e8061c4205e61a2c0515f5160206151df5f395f51905f5290820961c4a0526080615f8061c4c05e61a2e0515f5160206151df5f395f51905f52910961c54052608061974061c5605e8361c5e05260806197c061c6005e836173a051905f5160206151df5f395f51905f52910961c68052608061984061c6a05e836173c051905f5160206151df5f395f51905f52910961c720526080619cc061c7405e8461c7c0526080619e4061c7e05e846173a051905f5160206151df5f395f51905f52910961c860526080619ec061c8805e846173c051905f5160206151df5f395f51905f52910961c9005260806191c061c9205e8761c9a052608061924061c9c05e876173a051905f5160206151df5f395f51905f52910961ca405260806192c061ca605e876173c051905f5160206151df5f395f51905f52910961cae052608061934061cb005e876173e051905f5160206151df5f395f51905f52910961cb805260806193c061cba05e8761740051905f5160206151df5f395f51905f52910961cc2052608061944061cc405e8761742051905f5160206151df5f395f51905f52910961ccc05260806194c061cce05e8761744051905f5160206151df5f395f51905f52910961cd6052608061954061cd805e8761746051905f5160206151df5f395f51905f52910961ce005260806195c061ce205e8761748051905f5160206151df5f395f51905f52910961cea052608061964061cec05e876174a051905f5160206151df5f395f51905f52910961cf405260806196c061cf605e876174c051905f5160206151df5f395f51905f52910961cfe0526080619a4061d0005e8561d080526080619ac061d0a05e856173a051905f5160206151df5f395f51905f52910961d120526080619b4061d1405e856173c051905f5160206151df5f395f51905f52910961d1c0526080619bc061d1e05e856173e051905f5160206151df5f395f51905f52910961d260526080619c4061d2805e8561740051905f5160206151df5f395f51905f52910961d300526080616ac061d3205e868261d3a05261475a575b5f5160206151df5f395f51905f5296979387809693948180808080809a8199099c60808601350999606085013509966040840135099360208301350990350808080808616e60526080616b40616f005e6080612860815e805f5160206151df5f395f51905f52616e605181035f0861010052614743575b806080616e806101005e61472b575b6080616b406101005e80616a805161018052614712575b806146fa575b608080616f805e7c70616972696e672d62617463682d6163632d6b7a670000000000000000610100526080616f806101205e6080616f006101a05e6080616c406102205e6080616bc06102a05e5f5160206151df5f395f51905f5261022061010020069081156146f1575b6080616c406101005e8082610180526146d8575b806080616f806101805e6146be575b80916080616bc06101005e610180526146a5575b806080616f006101805e61468b575b80156100745761467f90614f24565b50600160805260206080f35b506080616f0061010080600b61c350fa60803d1416614670565b50608061010060a081600c61f230fa60803d1416614661565b506080616f8061010080600b61c350fa60803d141661464d565b50608061010060a081600c61f230fa60803d141661463e565b6001915061462a565b5060808061010081600b61c350fa60803d14166145bf565b50608061010060a081600c61f230fa60803d14166145b9565b5060808061010081600b61c350fa60803d14166145a2565b5060808060a081600c61f230fa60803d1416614593565b9692955090926080616e806130c061a300600c6208c678fa3d608014169592969391909361451c565b909194606060205f5160206151df5f395f51905f526001938180808c9b9a9b519b818d8c515109900899818c878c01515109900899604089015151099008970193019101612e37565b909194606060205f5160206151df5f395f51905f526001938180808c9b9a9b519b818d8c515109900899818c878c01515109900899604089015151099008970193019101612da1565b9091926020805f5160206151df5f395f51905f52600193818851885151099008950193019101612c07565b5f5160206151df5f395f51905f52826020600193019509926001600160801b0384168552019192612ac4565b91905f5160206151df5f395f51905f528086818460019509099280099201612a47565b5f5160206151df5f395f51905f5260019161030051900991828160051b61a3400152019061063d565b5f61a1c082015260200161062f565b909360205f5160206151df5f395f51905f5281928188358651099008950191016105e9565b5f5160206151df5f395f51905f52602091845190089201916105d9565b5f5160206151df5f395f51905f528382828060209587510988098552099101906105c4565b602091815f5160206151df5f395f51905f52809381038708855209910190859061056c565b915f5160206151df5f395f51905f5281600192099201610555565b9181355f5160206151df5f395f51905f52811015610074578152602090810192910190610506565b90928235915f5160206151df5f395f51905f5283101561007457828152918152602090810193928101929101610478565b919060806149d6838293614d69565b93818482370191019190610443565b919060806149f4838293614d69565b9381848237019101919061040d565b9190926080614a13838293614d69565b938184823701910192909291926103c4565b916080614a36858293969496614d69565b9481848237019101919291926103a0565b916080614a58858293969496614d69565b94818482370191019192919261038f565b91906080614a78838293614d69565b93818482370191019190610340565b614a95608092918392614d69565b92818582370192019190610308565b90602080918335945f5160206151df5f395f51905f528610169481520191016102f6565b506080616c4060a061a1c0600c61f230fa60803d14166102ca565b9050614af9600160381b600760386120c4615006565b9392614b0f600160381b6007603861210461514f565b5093919092169580614b53575b15614b2b575b505050506102b5565b909192948383178287171715151694616c4052616c6052616c8052616ca05283808080614b22565b95838317828617171516955f616c40525f616c60525f616c80525f616ca052614b1c565b915082915f616c40525f616c60525f616c80525f616ca0526102af565b506080616bc060a061a1c0600c61f230fa60803d141661022c565b9050614bc5600160381b60076038612044615006565b9392614bdb600160381b6007603861208461514f565b5093919092169580614c1f575b15614bf7575b50505050610217565b909192948383178287171715151694616bc052616be052616c0052616c205283808080614bee565b95838317828617171516955f616bc0525f616be0525f616c00525f616c2052614be8565b915082915f616bc0525f616be0525f616c00525f616c2052610211565b801561007457602061260052602061262052602061264052612660527f73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffffff612680525f5160206151df5f395f51905f526126a052602061260060c08160055afa156100745760203d03610074576126005190565b80356040820135908060801c610074578160801c610074576001600160801b038091169116906f1a0111ea397fe69a4b1ba7b6434bacd75f51602061521f5f395f51905f52602085013511158183141691101715610074576f1a0111ea397fe69a4b1ba7b6434bacd75f51602061521f5f395f51905f526060840135111581831416911017156100745760809060a03761012090565b81356040830135908060801c610074578160801c610074576001600160801b038091169116906f1a0111ea397fe69a4b1ba7b6434bacd75f51602061521f5f395f51905f52602086013511158183141691101715610074576f1a0111ea397fe69a4b1ba7b6434bacd75f51602061521f5f395f51905f52606085013511158183141691101715610074576080809282370190565b8015614f21575061a1c090616cc05191616ce0925b6170608410614efc5783515f5160206151df5f395f51905f5291098015614ef557602082526020808301526020604083015260608201527f73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffffff60808201525f5160206151df5f395f51905f5260a082015260208160c08160055afa60203d141692815191601f1901905b80616ce010614ecd5750505f5160206151df5f395f51905f5280616ce051830991616cc051900990616cc052616ce052565b5f5160206151df5f395f51905f5280835185099382519009928152601f199182019101614e9b565b505f925050565b60205f5160206151df5f395f51905f5281928694965190099283865201930190614e12565b90565b8015614f2157506080616f806103005e6101006128e06103805e6080616f006104805e6101006129e06105005e60206103008080600f62029810fa60203d141661030051161561007457600190565b5f96945f969293945f1901925f5b868110614f915750505050505050565b8483820483808260051b880135921516614ffe575b5087858406021c16868202610100811080614fdd575b15614fcc575b5050600101614f81565b60ff19011b9099019860015f614fc2565b9a82821b019a6101008983011115614fbc579b8282610100031c019b614fbc565b900383614fa6565b600193925f926003820160021c845b81811061510957505084833510156001166150c2575b9360049184958261503d960294614f73565b8192919381936f1a0111ea397fe69a4b1ba7b6434bacd781145f51602061521f5f395f51905f5284148116806150b7575b156150a7575b6f1a0111ea397fe69a4b1ba7b6434bacd7905f51602061521f5f395f51905f52600160801b891095111516911017161693565b6001860195861090960195615074565b5f975087965061506e565b9361503d9350815f51602061521f5f395f51905f526f1a0111ea397fe69a4b1ba7b6434bacd76150f88460048181988c8b614f73565b92909214911416945091509361502b565b828160021b8503600490818110615147575b50026101008110615130575b50600101615015565b9760018092991b8960051b87013510169790615127565b90505f61511b565b9290915f926001946003830160021c5f5b818110615178575050925f9260049261503d95614f73565b838160021b86036004908181106151b6575b5002610100811061519f575b50600101615160565b9760018092991b8960051b85013510169790615196565b90505f61518a56fe4e5280109d8f96b8bfb543a6b1af25fb56a9db616af85a90eedc558e3eb1ea2973eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff000000014997c5aa3a5fa07bcaf880a9054bef831effbd9cd58e46d9bb4fb88ef99de0db64774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff000000004382d0938a760120dd6cef8f3b90a0c38abae475e3d21e39365472b76d780272 +0x6108e06040526004361015610012575f80fd5b5f3560e01c80631e8e1e1314610078576342b7259714610030575f80fd5b34610074575f366003190112610074576040517f0000000000000000000000000d2789bcdf4a0406dc7383bd7d4b73a7f08cac166001600160a01b03168152602090f35b5f80fd5b346100745760403660031901126100745760043567ffffffffffffffff8111610074573660238201121561007457806004013567ffffffffffffffff81116100745760243691830101116100745760243567ffffffffffffffff8111610074573660238201121561007457806004013567ffffffffffffffff8111610074576024369160051b8301011161007457611ec060409114911416156100745760017f0000000000000000000000000d2789bcdf4a0406dc7383bd7d4b73a7f08cac165a8260f81b175f80a17f24430c63ec2017f12ec4004d66c66314f418dd77fe38dd4c2475c8f2b259e009813f14614281823b1416156100745781612700614280923c6013612720511481166014612740511416816127e0511416600b61280051141660076128205114166038612840511416801561007457611e6060443514166013611ec43514163661214414168015610074575f90731a0111ea397fe69a4b1ba7b6434bacd764774b846120a435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa612084351416731a0111ea397fe69a4b1ba7b6434bacd764774b8461206435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa6120443514161680614d69575b15614cd5575b166080616bc061a1c05e808261a24052614cbc575b5f90731a0111ea397fe69a4b1ba7b6434bacd764774b8461212435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa612104351416731a0111ea397fe69a4b1ba7b6434bacd764774b846120e435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa6120c43514161680614c9f575b15614c0b575b1680916080616c4061a1c05e61a24052614bf2575b8015610074575a600160f91b175f80a160806127005190525f60a0525f60c0525f60e0525f61010052601361012052610140611ee45b6121448110614bce57508115610074575a600360f81b175f80a16064906191c0905b826107e48110614bb1575f5160206153025f395f51905f5285925a600160fa1b175f80a1607f190160802080608052066169805260a090619940916101008201925b838310614b935784835f5160206153025f395f51905f52845a600560f81b175f80a1607f190160802080608052066169a0525f5160206153025f395f51905f52602060802080608052066169c05260a0619a4061030083015b808410614b715750505a600360f91b175f80a1619d40608083015b808410614b4f5750610100608061042e858095614e8f565b948181619e4037019201905b818310614b2d5750505f5160206153025f395f51905f52608061045e838095614e8f565b928181619ec03701915a600760f81b175f80a1607f190160802080608052066169e05260a0610100619f409301925b838310614b0f5784835f5160206153025f395f51905f52845a600160fb1b175f80a1607f19016080208060805206616a005260a090619fc0916102008201925b838310614af15784835f5160206153025f395f51905f52845a600960f81b175f80a1607f19016080208060805206616a205260a090618500610cc08201905b818310614ac05750505f5160206153025f395f51905f528192607f19016080208060805206616a40525f5160206153025f395f51905f5260206080208060805206616a6052610120608061055f83614df9565b928181616ac0370191607f190160802092836080526001600160801b035f5160206153025f395f51905f5260a0950616616a8052826182a052015b808210614a9857506080905f5160206153025f395f51905f52611ec493607f1901832080845206616aa0526105ce81614df9565b508181616b40370103610074575a600560f91b175f80a1616a205180915f5b60148110614a7d57506127805192616cc0846127c0515b6170608310614a58575050505061063d5f5160206153025f395f51905f525f5160206153625f395f51905f528408918261706052614f23565b925f5160206153025f395f51905f52616cc092612760519009916127c0515b6170608210614a33578585616ce05190616d00915b616e008310614a16575f92611ee4905b61214482106149f157505061706051616cc05190616e005193616cc052616ce052616d0052616d2052616d4052616d60525a600b60f81b175f80a1801561007457616a0051610300525f61a300525f5b61014081106149e257506001805b603182106149b95782618b4051618a40516185205190618a6051916185405161088052618a8051936185605194618aa0516108c0526185805190618ac0516185a0516108a052618ae05191886185c0519586946108805189618b0051905f5160206153025f395f51905f529109905f5160206153025f395f51905f529109955f5160206153025f395f51905f529109936108a0515f5160206153025f395f51905f52910992866108c051905f5160206153025f395f51905f529109925f5160206153025f395f51905f52910990610880515f5160206153025f395f51905f52908c09905f5160206153025f395f51905f528a8c095f5160206153025f395f51905f529108905f5160206153025f395f51905f529108905f5160206153025f395f51905f529108905f5160206153025f395f51905f529108905f5160206153025f395f51905f529108905f5160206153025f395f51905f529108905f5160206153025f395f51905f5291088684618b2051905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f5291085f5160206153025f395f51905f5290600109956103005161a30051905f5160206153025f395f51905f5291099661a1c051905f5160206153025f395f51905f52910861a1c0526108a0515f5160206153025f395f51905f52035f5160206153025f395f51905f52905f08915f5160206153025f395f51905f52035f5160206153025f395f51905f52905f089061088051905f5160206153025f395f51905f529108905f5160206153025f395f51905f529108905f5160206153025f395f51905f5291085f5160206153025f395f51905f529060010961a1e051905f5160206153025f395f51905f52910861a1e0525f5160206153025f395f51905f52035f5160206153025f395f51905f52905f08915f5160206153025f395f51905f529108905f5160206153025f395f51905f5291085f5160206153025f395f51905f529060010961a20051905f5160206153025f395f51905f529108906185e0515f5160206153025f395f51905f52035f5160206153025f395f51905f52905f089061088051905f5160206153025f395f51905f529108905f5160206153025f395f51905f5291085f5160206153025f395f51905f5290600109918261a9605261030051906103005190610300515f5160206153025f395f51905f529109905f5160206153025f395f51905f529109905f5160206153025f395f51905f52910961a3005261a360515f5160206153025f395f51905f529109905f5160206153025f395f51905f52910861a20052614120905f9061a960825b84906152ef861015611f7057600186515f1a9601959182600514611f44575081600614611f245781600814611f095781600d14611edf5781601014611ebb5781601114611e975781602114611bf85750806019146118ad5780601f146113575780601b14610b9857600b14610b27575f80fd5b600384519401935f905f5160206153025f395f51905f526103005161a300510961a300525f5160206153025f395f51905f5285611fe08360f31c1661a1c0019283519061ffff8160e81c16610b80575b50089052610ab4565b90621fffe0849260e31c1661a3400151900989610b77565b505082516002909301925f925061a96090839060f01c8015610ec45780600114610d985780600214610cb357600314610bcf575f80fd5b5f5160206153025f395f51905f528080808080618b0051816185e05181035f0890088180618520518161858051918009097f404d21073985d14e432a4ad76d3fae06ca74314b950fe7b1d7f501cd31a8b374099008818061854051816185a051918009097f0b2cc8704264c6bd81bc620e9e524d4b73e9b2317679422ff7fa1603955649f10990088180618560518161862051918009097f0fdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f099008600109808452816103005161a300510961a300528161a2e05161a3605190090861a2e052610ab4565b505f5160206153025f395f51905f528080808080618ae051816185c05181035f0890088180618520518161858051918009097f1b8114c381b922fd5d6d241210e2d8a68ad5744053ba9e776118de4107b51ace099008818061854051816185a051918009097f3df32e4cc4cb2ed20e5d21899cf5331775990ccaec4c09b4e3717213fcc0d7630990088180618560518161862051918009097f3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc1099008600109808452816103005161a300510961a300528161a2e05161a3605190090861a2e052610ab4565b505f5160206153025f395f51905f528080807f73eda753299d7d483339d80809a1d7f7b67900f7fe6bfad98998021b7bb5732b81807f73eda753299d7d483339d80809a1d80553bca402fffe5bfeffffffff0000000181808080600160701b61852051088180600160701b6185405108600160381b0990088180600160701b6185605108600160701b099008818080618660518161868051600160381b0990086186a0518290600160701b09900881035f08900808818070241eabfffeb153ffffb9feffffffffaaab6187c0510981035f089008088180600160861b817f73eda753299d7d483339d80809a1d80553bda402fffe5b6e855000003ab000026187e051080981035f089008600109808452816103005161a300510961a3005261a240510861a24052610ab4565b5061852051610400526185c051610520526185e051610480526186005161044052618540516104a0526187a0516104c0525f5160206153025f395f51905f528080807f73eda753299d7d483339d80809a1d7d340dd972492de594de627fffefcb803498180618560516187805161054052618580516105805261876051610460526185a0516104205261874051610500526186205161056052618640516104e05281808080618660518161868051600160381b0990086186a0518290600160701b09900881035f089181808061044051600160701b09818061048051600160381b096105205108089181808083600160701b0981806104a051600160381b09610400510808918180806104c0516104e05109700c8557e86f90d0d89eed6eb5349a0f88200991818080610540516104e05109702cb9b546d20373eaf85e8f53db883cb5480991818080610460516104e0510970013af65741744bd7bb2c6872df2b8003200991818080610500516104e0510970340f2ebe380a0f5eff4360543988a61dc20991818080610440516104e0510970297784894e27525bc342b7fde37dba93660991818080610480516104e05109703212e00cde6d2002b119d800000347fcb809918180806104c0516105605109702cb9b546d20373eaf85e8f53db883cb548099181808061054051610560510970013af65741744bd7bb2c6872df2b800320099181808061046051610560510970340f2ebe380a0f5eff4360543988a61dc2099181808061050051610560510970297784894e27525bc342b7fde37dba93660991818080610440516105605109703212e00cde6d2002b119d800000347fcb809918180806104c051610420510970013af65741744bd7bb2c6872df2b800320099181808061054051610420510970340f2ebe380a0f5eff4360543988a61dc2099181808061046051610420510970297784894e27525bc342b7fde37dba93660991818080610500516104205109703212e00cde6d2002b119d800000347fcb809918180806104c051610580510970340f2ebe380a0f5eff4360543988a61dc2099181808061054051610580510970297784894e27525bc342b7fde37dba93660991818080610460516105805109703212e00cde6d2002b119d800000347fcb809918180806104c051840970297784894e27525bc342b7fde37dba936609918180808080610540518609703212e00cde6d2002b119d800000347fcb80993610520519009600160701b098180806104c0516104a05109703212e00cde6d2002b119d800000347fcb809818080610480516104a05109600160701b09818080610520516104a05109600160381b09818080610440516104005109600160701b09818080610480516104005109600160381b09816105205161040051090808080808080808080808080808080808080808080808080808080808818070241eabfffeb153ffffb9feffffffffaaab6187c0510981035f089008088180600160861b817f73eda753299d7d483339d80809a1d80553b9202d7ffe85d4800008bb200000016187e051080981035f089008600109808452816103005161a300510961a3005261a220510861a22052610ab4565b505090505f905f61a96090616d40516103a052616d005161032052616d20516103e0526169a05161038052616980516103c0525f5160206153025f395f51905f52806190e05181610320516103a0510809816103005161a30051090861a300525f5160206153025f395f51905f52618b6051816103c05191816103c0515f0908095f5b6004811061188057505060015f5b6004811061185e5750600161a9e05260015b6004811061182c5750600161aac0526003805b6117fa57505f905f5b600481106117cd5750905f5160206153025f395f51905f52808080808096816190c05197810391880908816103005161a3005109089381808080618be051948180618b8051816103c0515f090881618ba051916103c05190090895096190e0510881036191005108816190a0519361038051900890090881806103e05161032051088103600108099161030051900908610360526191605161034052618a00516102e05261852051610260526185405161028052618560516102c052618580516102a0526185a0516102405261862051610220526186405161020052618660516101e052618680516101c0526186a0516101a0526186c051610180526186e0516101605261870051610140526187205161012052618bc051610100526191405160e0525f5160206153025f395f51905f5280618c005181035f0860010860c0525f5160206153025f395f51905f52808060e05160010961034051088103619180510860a0525f5160206153025f395f51905f5280806191205181806103805181806101005160c05109816103c05181806101205160c05109816103c05181806101405160c05109816103c05181806101605160c05109816103c05181806101805160c05109816103c05181806101a05160c05109816103c05181806101c05160c05109816103c05181806101e05160c05109816103c05181806102005160c05109816103c05181806102205160c05109816103c05181806102405160c05109816103c05181806102a05160c05109816103c05181806102c05160c05109816103c05181806102805160c05109816103c05181806102605160c05109816103c05181806102e05160c05109816103c0515f09080908090809080908090809080908090809080908090809080908090809080860a051090881806103e0516103205108810360010809816103005181805f5160206153625f395f51905f528180610380518161010051816103c0518161012051816103c0518161014051816103c0518161016051816103c0518161018051816103c051816101a051816103c051816101c051816103c051816101e051816103c0518161020051816103c0518161022051816103c0518161024051816103c051816102a051816103c051816102c051816103c0518161028051816103c0518161026051816103c051816102e051816103c0515f09080908090809080908090809080908090809080908090809080908090809080860e0510908816103005181806103405181610320516103a051080981610300516103605109080908090861a30052610ab4565b915f5160206153025f395f51905f52600191818560051b8061aa6001519061a9e001510990089201611416565b5f5160206153025f395f51905f528160051b808601519061aa600151095f19820160051b61aa6001525f19018061140d565b6001905f5160206153025f395f51905f525f19820160051b808701519061a9e00151098160051b61a9e00152016113fa565b905f5160206153025f395f51905f526001918360051b860151900991016113e8565b8060019160051b5f5160206153025f395f51905f52610380518183618540015187080890860152016113da565b5050618a205161a9609081525f925082805b60058110611bdf57506185005161aa2052616d605161aa40525f5b60098110611bc65750618a005161ab80525f5b60128110611bad57505f5b60068110611b9257505f5b60068110611b7757505f5b60058110611b5c57505f5160206153025f395f51905f5280808061ade0518103600108616d405109816103005161a30051090881808061ae80518181810391800908616d005109916103005190090861a3005260015b60068110611b1657505f5160206153025f395f51905f52616a20516169a0510961b000525f5b600681106119985750610ab4565b600381026003810160128111611b0e575b8260051b908161aea001519161ade001519061b00051908194906169c051906169a0515b8a828510611a4157505050505050600193925f5160206153025f395f51905f52808080957f4285088329c399ea457a8ca1d30f8957e74c7f529842a1579b4fee55b398292395820390088180616d2051616d0051088103890809816103005161a30051090861a300520961b000520161198a565b9285979385969792939482809760051b809301519261aba001515f5160206153025f395f51905f529087095f5160206153025f395f51905f52908408905f5160206153025f395f51905f5291085f5160206153025f395f51905f529109985f5160206153025f395f51905f529108905f5160206153025f395f51905f5291085f5160206153025f395f51905f529109947f08634d0aa021aaf843cab354fabb0062f6502437c6a09c006c083479590189d75f5160206153025f395f51905f529109936001019291906119cd565b5060126119a9565b805f5160206153025f395f51905f52808060019460051b61ade001515f19850160051b61af60015182039008616d405109816103005161a30051090861a3005201611964565b80606060019202618ec001518160051b61af6001520161190e565b80606060019202618ea001518160051b61aea0015201611903565b80606060019202618e8001518160051b61ade00152016118f8565b8060019160051b80618c4001519061aba00152016118ed565b8060019160051b8061862001519061aa600152016118da565b8060019160051b8061852001519061a9800152016118bf565b92939480915090600181515f1a91015f9260018316611e87575b505f9360028316611e6e575b815196875f1a8860011a908960021a9a60058b60041a960199611e60575b505f5b818110611e135750505f5b818110611db45750505f5b878a821015611cec5788519860118a60f01c9101995f5b60078110611c805750505050600101611c55565b8060051b8301515f6080525b600760805110611c9f5750600101611c6c565b9a5f5160206153025f395f51905f5290818d81600460805187018a0101515f1a60051b612ae001519160805160051b61ffff8960e01c16015190090990089a600160805101608052611c8c565b5050959750955f905b8060031a8210611d7a5750505f905b808210611d36575050600116611d1e575b50916001610ab4565b905f5160206153025f395f51905f5291510984611d15565b90935f5160206153025f395f51905f526001918160058b519b019a81815f1a60051b612ae001519161ffff808260d81c16519160e81c165109099008940190611d04565b90945f5160206153025f395f51905f526001918160038c519c019b61ffff8160e81c1651905f1a60051b612ae00151099008950190611cf5565b6002895160f01c990198515f905b8a60078310611dd657505050600101611c4a565b600191929a5f5160206153025f395f51905f5260038193519e019d8161ffff825f1a60051b612ae001519260e81c16518709099008990190611dc2565b5f5b8a60078210611e28575050600101611c3f565b6001919a5f5160206153025f395f51905f5260038193519e019d61ffff8160e81c1651905f1a60051b612ae001510990089901611e15565b84526020909301928b611c3c565b9350600181515f1a91019060051b612ae0015193611c1e565b905160f01c925060030187611c12565b935f5160206153025f395f51905f5291506002865160f01c96019551900992610ab4565b935f5160206153025f395f51905f5291506002865160f01c96019551900892610ab4565b935f5160206153025f395f51905f529150600186515f1a96019560051b612ae00151900992610ab4565b935f5160206153025f395f51905f52809250035f0892610ab4565b93915f5160206153025f395f51905f529150601f19019182510892610ab4565b92949150946003905160f01c920194611f62575b5051916001610ab4565b835260209092019184611f58565b83906152ef870361007457610074576169e051610840525f5160206153025f395f51905f52618ae051815f5160206153625f395f51905f526185c051099008610720526185205161082052618540516108005261856051610760525f5160206153025f395f51905f526107605161076051096107e05261858051610860525f5160206153025f395f51905f52610860516108605109610740526185a0516107a0525f5160206153025f395f51905f526107a0516107a051096107805261862051610700525f5160206153025f395f51905f526107005161070051096107c052618640516106e0525f5160206153025f395f51905f526106e0516106e051096106c052618660516106a0525f5160206153025f395f51905f526106a0516106a05109610680525f5160206153025f395f51905f52618b0051815f5160206153625f395f51905f526185e05109900861066052618b205161064052618b405161062052618a405161060052618a60516105e052618a80516105c0525f5160206153025f395f51905f52618aa051815f5160206153625f395f51905f52618600510990086105a0525f5160206153025f395f51905f5280808080618c205181036001086191a0519009810381808080806106805161068051096106a051095f5160206153225f395f51905f5209818080806106c0516106c051096106e051095f5160206153825f395f51905f5209818080806107c0516107c0510961070051095f5160206152e25f395f51905f5209818080806107805161078051096107a051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e098180808061074051610740510961086051097ed94c46a8456352aa44d7a885ab59e3a36664e6fb25e826f8a4cd79822f053309818080806107e0516107e0510961076051097f1f61345b652161410c5e29f51e301ae56342af824bc110649393d2b911c50d3e098180610800517f40fa389feb2522bb934881ac9ed749aee2296502af592418c6b5675c0f560261098180610820517f70d8f2a733a64d650faccc9b1c2a766a9544bb3ff1a11ee73cb43947ef386633096105a0510808080808080808816108405181808080806106c0516106c051096106e051095f5160206153225f395f51905f5209818080806107c0516107c0510961070051095f5160206153825f395f51905f5209818080806107805161078051096107a051095f5160206152e25f395f51905f52098180808061074051610740510961086051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e09818080806107e0516107e0510961076051097ed94c46a8456352aa44d7a885ab59e3a36664e6fb25e826f8a4cd79822f0533098180610800517f27e7119226c42a6d19c1541904b99ae40685511ed2e078964b74594d38340849098180610820517f6d05a41959f539a7fc9ec0972ea1e3dbb6fc67dd51daf3414f7fbbb091c7274a0981805f5160206153625f395f51905f526106a051096105c0510808080808080808816108405181808080806107c0516107c0510961070051095f5160206153225f395f51905f5209818080806107805161078051096107a051095f5160206153825f395f51905f52098180808061074051610740510961086051095f5160206152e25f395f51905f5209818080806107e0516107e0510961076051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e098180610800517f23a6684b942d726a22e4d5b8d8ff83aeaa773f62600184efe5d033d7c7c6e827098180610820517f2f5908b169c6cf1bd26dcf0f9e5105481f5164f3ece0582bf3098312167751a70981805f5160206153625f395f51905f526106e051096105e05108080808080808816108405181808080806107805161078051096107a051095f5160206153225f395f51905f52098180808061074051610740510961086051095f5160206153825f395f51905f5209818080806107e0516107e0510961076051095f5160206152e25f395f51905f52098180610800517f24822e1af9aa2887c912c87eb0f20bd332330e7e55cd784de67cb407a9f05520098180610820517f726df1506749848155630b86ae25a82b281ecd050fe3a52d85a181fa87202e4b0981805f5160206153625f395f51905f526107005109610600510808080808088161084051818080808061074051610740510961086051095f5160206153225f395f51905f5209818080806107e0516107e0510961076051095f5160206153825f395f51905f52098180610800517f26c2cc87f95726b28f33ca03409a460ec987cfe12adae32769e3565865d07191098180610820517f222e83e70453dfee19b402e9fa8dfe2c4987b034d0be3ceb478b3022e97934c10981805f5160206153625f395f51905f526107a05109610620510808080808816108405181808080806107e0516107e0510961076051095f5160206153225f395f51905f52098180610800517f6bd72f9cfc53af9d931896e77ea5c61244cb6d5fae8954f37dc7b9002f5aa78a098180610820517f5e1d3dbecda6214343e24a47f45c5d033197ad01b65a730af95dc57e90c491400981805f5160206153625f395f51905f5261086051096106405108080808816108405181808080806106805161068051096106a051097f0fdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f09818080806106c0516106c051096106e051097f301cf56f9b4577112cc4241cddf6484aaadedbf1bbd0f2351adf2e41c2fb2ecd09818080806107c0516107c0510961070051097f5f3a15bab4ce4097b1edc3a25002694b92395ce355a8a12fe557459d9633f70109818080806107805161078051096107a051097f275a20361ea91992193920270d3e2d1f6361880ac0a439c64bef815d4469ba85098180808061074051610740510961086051097f31e823a45e567484c1544e310c0fa5cd66547a8f0dde659ac61698c30e838d2509818080806107e0516107e0510961076051097f26cc223e16f47c20e17cc6069605fa5a8af05ea4f6eb36029a641d23b818eb10098180610800517f4d0ea7f9c3fda06d9535b0fdafd8338bd47c2200b284fa71a325ff41ac358028098180610820517f5b1fc262a28cbb8bf75d9b1a6edaa74591ec24cd9a209512213cec3a3c0f1a5d09610660510808080808080808816108405181808080806106805161068051096106a051097f3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc109818080806106c0516106c051096106e051097f06ccb1c7d87f3c12a2bde4e68ac7f1e8b03481ba15d7f88f9a7f9b8310dd6d3409818080806107c0516107c0510961070051097f53fded36d490ba6b05a5d10fd99ffe5456baec6a6a8753199d5ebdc33c99790e09818080806107805161078051096107a051097f412c98232b6ab8a47aa76ee814ef7ec6261987c9802f2cfc490e007951a60ca5098180808061074051610740510961086051097f333f8046ece5579cbd6872449c57f2703dfc8864cfadc06d587ff104a0d0c1f209818080806107e0516107e0510961076051097f3509dd2fe3aac0080783557fec090fb1cb4b2b0901253c55282024331d1fe1a8098180610800517f52f789e4afc3801f7411102ee2f47cc5954a744e71cac98e75ea962a55a0a76f098180610820517f590ba402032e82eb1f660ef09796c5686345a5054ed96dae8e2d2336337887710961072051080808080808080881610840515f0908090809080908090809080908090808816103005161a3005109088161a9405161a1c0510961a1c0528161a9205161a1e0510961a1e0528161a8c05161a200510961a200528161a8605161a220510961a220528161a8005161a240510961a240528161a7a05161a260510961a260528161a7405161a280510961a280528161a6e05161a2a0510961a2a0528161a6805161a2c0510961a2c0528161a5c05161a2e0510961a2e05281035f08616d80525a600360fa1b175f80a1616a2051908160015f5b6014811061499657505f5160206153025f395f51905f5280808080888180988198616da0528103600108616dc0525a600d60f81b175f80a181808061278051816127a051809c81809c81809c818c819d9b829c9a839b61704052820961706052098061702052090909090909090909617000525a601160f81b175f80a1616a4051600161738052600190617380905f915b602a831061496a57845a600960f91b175f80a1618a0061a3005261850061a320526190a061a340526190c061a3605261912061a3805261914061a3a0526191a061a3c052618a2061a3e052618a4061a40052618a6061a42052618a8061a44052618aa061a46052618ac061a48052618ae061a4a052618b0061a4c052618b2061a4e052618b4061a50052618b6061a52052618b8061a54052618ba061a56052618bc061a58052618be061a5a052618c0061a5c052618c2061a5e052618c4061a60052618c6061a62052618c8061a64052618ca061a66052618cc061a68052618ce061a6a052618d0061a6c052618d2061a6e052618d4061a70052618d6061a72052618d8061a74052618da061a76052618dc061a78052618de061a7a052618e0061a7c052618e2061a7e052618e4061a80052618e6061a82052616d8061a84052618a00516173a061a32060015b602b811061493f57505050617ba0525a601360f81b175f80a16186e0515f5160206153025f395f51905f526189a0519181806173a051928184618700510990089381836189c05109900882806173c051958187618720510990089181866189e05109900890617bc052617be0525a600560fa1b175f80a1818080619060518180619080519281886190e051099008956191005109900892818661916051099008936191805109900890617c0052617c20525a601560f81b175f80a161852061a300526185c061a3205261884061a3405261854061a360526185e061a3805261886061a3a05261856061a3c05261860061a3e05261888061a4005261858061a4205261874061a440526188a061a460526185a061a4805261876061a4a0526188c061a4c05261862061a4e05261878061a500526188e061a5205261864061a540526187a061a5605261890061a5805261866061a5a0526187c061a5c05261892061a5e05261868061a600526187e061a6205261894061a640526186a061a6605261880061a6805261896061a6a0526186c061a6c05261882061a6e05261898061a70052618520516185c05161884051916173a061a36060015b600b81106148f657505050617c4052617c6052617c80525a600b60f91b175f80a1618e8061a30052618ea061a32052618ec061a34052618ee061a36052618f0061a38052618f2061a3a052618f4061a3c052618f6061a3e052618f8061a40052618fa061a42052618fc061a44052618fe061a4605261900061a4805261902061a4a05261904061a4c052618e8051618ea051618ec051916173a061a36060015b600581106148ad57505050617ca052617cc052617ce0525a601760f81b175f80a1616a6051616a80516182a0516170005191836170205181617040519581617060519188815f5160206153025f395f51905f52035f5160206153025f395f51905f529089085f5160206153025f395f51905f528581038a085f5160206153025f395f51905f528381038b08905f5160206153025f395f51905f529109905f5160206153025f395f51905f529109915f5160206153025f395f51905f5281810383085f5160206153025f395f51905f5286810384085f5160206153025f395f51905f5290600109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52908409895f5160206153025f395f51905f5283810388085f5160206153025f395f51905f5285810389085f5160206153025f395f51905f5290600109905f5160206153025f395f51905f5291095f5160206153025f395f51905f5290830994878d5f5160206153025f395f51905f5282810387085f5160206153025f395f51905f5288810388085f5160206153025f395f51905f5290600109905f5160206153025f395f51905f5291095f5160206153025f395f51905f5290890961310090614d86565b955f5160206153025f395f51905f5283810382085f5160206153025f395f51905f5289810383085f5160206153025f395f51905f5290600109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52908809935f5160206153025f395f51905f5282810385085f5160206153025f395f51905f528a810386085f5160206153025f395f51905f5290600109905f5160206153025f395f51905f5291095f5160206153025f395f51905f529086095f5160206153025f395f51905f528381038b085f5160206153025f395f51905f528681038c085f5160206153025f395f51905f5290600109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52908209995f5160206153025f395f51905f5286810389085f5160206153025f395f51905f528281038a08905f5160206153025f395f51905f5291095f5160206153025f395f51905f52908c099a8b945f5160206153025f395f51905f52035f5160206153025f395f51905f52908a085f5160206153025f395f51905f529109905f5160206153025f395f51905f52035f5160206153025f395f51905f529089085f5160206153025f395f51905f529082099788965f5160206153025f395f51905f52035f5160206153025f395f51905f5291085f5160206153025f395f51905f529109915f5160206153025f395f51905f52910981617ca051905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f5203935f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52916080013509905f5160206153025f395f51905f529108925f5160206153025f395f51905f52910990617cc051905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52035f5160206153025f395f51905f529108925f5160206153025f395f51905f52910990617ce051905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52035f5160206153025f395f51905f5291085f5160206153025f395f51905f52825f09905f5160206153025f395f51905f52910887895f5160206153025f395f51905f5285810388085f5160206153025f395f51905f5282810389085f5160206153025f395f51905f5290600109905f5160206153025f395f51905f5291095f5160206153025f395f51905f528881038b085f5160206153025f395f51905f528781038c085f5160206153025f395f51905f528481038d08905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291098a5f5160206153025f395f51905f528a810385085f5160206153025f395f51905f5289810386085f5160206153025f395f51905f5290600109905f5160206153025f395f51905f5291095f5160206153025f395f51905f5290830991885f5160206153025f395f51905f528c810382085f5160206153025f395f51905f5287810383085f5160206153025f395f51905f5290600109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52908509968c5f5160206153025f395f51905f52878a096135c790614d86565b965f5160206153025f395f51905f52908809935f5160206153025f395f51905f5282810385085f5160206153025f395f51905f528a810386085f5160206153025f395f51905f5290600109905f5160206153025f395f51905f5291095f5160206153025f395f51905f529086095f5160206153025f395f51905f528381038b085f5160206153025f395f51905f528681038c085f5160206153025f395f51905f5290600109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52908209995f5160206153025f395f51905f5286810389085f5160206153025f395f51905f528281038a08905f5160206153025f395f51905f5291095f5160206153025f395f51905f52908c099a8b945f5160206153025f395f51905f52035f5160206153025f395f51905f52908a085f5160206153025f395f51905f529109905f5160206153025f395f51905f52035f5160206153025f395f51905f529089085f5160206153025f395f51905f529082099788965f5160206153025f395f51905f52035f5160206153025f395f51905f5291085f5160206153025f395f51905f529109915f5160206153025f395f51905f52910981617c4051905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f5203935f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52916060013509905f5160206153025f395f51905f529108925f5160206153025f395f51905f52910990617c6051905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52035f5160206153025f395f51905f529108925f5160206153025f395f51905f52910990617c8051905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52035f5160206153025f395f51905f529108915f5160206153025f395f51905f529109905f5160206153025f395f51905f529108905f5160206153025f395f51905f5281810389085f5160206153025f395f51905f52906001095f5160206153025f395f51905f5282810388085f5160206153025f395f51905f528a81038908905f5160206153025f395f51905f529109905f5160206153025f395f51905f529109905f5160206153025f395f51905f5289810382085f5160206153025f395f51905f52906001095f5160206153025f395f51905f5290830961397a90614d86565b5f5160206153025f395f51905f528a810383085f5160206153025f395f51905f52906001095f5160206153025f395f51905f52908209915f5160206153025f395f51905f528181038c085f5160206153025f395f51905f52906001095f5160206153025f395f51905f52908409905f5160206153025f395f51905f528c81038b085f5160206153025f395f51905f529083099384925f5160206153025f395f51905f528381038d085f5160206153025f395f51905f529109915f5160206153025f395f51905f52035f5160206153025f395f51905f52908c085f5160206153025f395f51905f528e81038d08905f5160206153025f395f51905f5291095f5160206153025f395f51905f52910981617c0051905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f5203915f5160206153025f395f51905f5291095f5160206153025f395f51905f529060408c013509905f5160206153025f395f51905f529108925f5160206153025f395f51905f52910990617c2051905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52035f5160206153025f395f51905f529108915f5160206153025f395f51905f529109905f5160206153025f395f51905f529108905f5160206153025f395f51905f5281810387085f5160206153025f395f51905f52906001095f5160206153025f395f51905f5282810386085f5160206153025f395f51905f528881038708905f5160206153025f395f51905f5291095f5160206153025f395f51905f52828209925f5160206153025f395f51905f5289810382085f5160206153025f395f51905f52906001095f5160206153025f395f51905f52908509613c1b90614d86565b915f5160206153025f395f51905f528a810383085f5160206153025f395f51905f52906001095f5160206153025f395f51905f52908409935f5160206153025f395f51905f52908509935f5160206153025f395f51905f528b81038a085f5160206153025f395f51905f529086099485935f5160206153025f395f51905f52035f5160206153025f395f51905f52908b085f5160206153025f395f51905f529109915f5160206153025f395f51905f52910981617bc051905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f5203915f5160206153025f395f51905f5291095f5160206153025f395f51905f529060208a013509905f5160206153025f395f51905f529108925f5160206153025f395f51905f52910990617be051905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52035f5160206153025f395f51905f529108915f5160206153025f395f51905f529109905f5160206153025f395f51905f529108925f5160206153025f395f51905f52035f5160206153025f395f51905f529108613ddc90614d86565b90617ba0515f5160206153025f395f51905f52039035905f5160206153025f395f51905f529108905f5160206153025f395f51905f529109915f5160206153025f395f51905f529109905f5160206153025f395f51905f5291089081616e40525a600360fb1b175f80a1616aa05180808094616da051616dc051916182a051925f5160206153025f395f51905f52856001096001600160801b038116955f5160206153025f395f51905f5291096001600160801b038116965f5160206153025f395f51905f5291096001600160801b038116995f5160206153025f395f51905f5291096001600160801b038116975f5160206153025f395f51905f5291096001600160801b03169260806198c061a3005e600161a38052608061994061a3a05e6173c05161a420526080619d4061a4405e6173e05161a4c05260806199c061a4e05e6174005161a560526080619dc061a5805e6174205161a600526080619f4061a6205e6174405161a6a052608061578061a6c05e6174605161a74052608061550061a7605e6174805161a7e052608061558061a8005e6174a05161a88052608061560061a8a05e6174c05161a92052608061568061a9405e6174e05161a9c052608061570061a9e05e6175005161aa6052608061530061aa805e6175205161ab0052608061538061ab205e6175405161aba052608061540061abc05e6175605161ac4052608061548061ac605e6175805161ace052608061580061ad005e6175a05161ad8052608061588061ada05e6175c05161ae2052608061590061ae405e6175e05161aec052608061598061aee05e6176005161af60526080615b8061af805e6176205161b000526080615f0061b0205e6176405161b0a052608061600061b0c05e6176605161b14052608061608061b1605e6176805161b1e052608061610061b2005e6176a05161b28052608061618061b2a05e6176c05161b32052608061620061b3405e6176e05161b3c052608061628061b3e05e6177005161b46052608061630061b4805e6177205161b50052608061638061b5205e6177405161b5a052608061640061b5c05e6177605161b64052608061648061b6605e6177805161b6e052608061650061b7005e6177a05161b78052608061658061b7a05e6177c05161b82052608061660061b8405e6177e05161b8c052608061668061b8e05e6178005161b96052608061670061b9805e6178205161ba0052608061678061ba205e6178405161baa052608061680061bac05e6178605161bb4052608061688061bb605e6178805161bbe052608061690061bc005e6178a05161bc80526178c051915f5160206153025f395f51905f529083096080619fc061bca05e818161bd20525f5160206153025f395f51905f529109608061a04061bd405e818161bdc0525f5160206153025f395f51905f52910990608061a0c061bde05e8161be6052608061a14061be805e5f5160206153025f395f51905f52910961bf00526080615a0061bf205e61a1c0515f5160206153025f395f51905f5290820961bfa0526080615a8061bfc05e61a1e0515f5160206153025f395f51905f5290820961c040526080615b0061c0605e61a200515f5160206153025f395f51905f5290820961c0e0526080615c0061c1005e61a220515f5160206153025f395f51905f5290820961c180526080615c8061c1a05e61a240515f5160206153025f395f51905f5290820961c220526080615d0061c2405e61a260515f5160206153025f395f51905f5290820961c2c0526080615d8061c2e05e61a280515f5160206153025f395f51905f5290820961c360526080615e0061c3805e61a2a0515f5160206153025f395f51905f5290820961c400526080615e8061c4205e61a2c0515f5160206153025f395f51905f5290820961c4a0526080615f8061c4c05e61a2e0515f5160206153025f395f51905f52910961c54052608061974061c5605e8361c5e05260806197c061c6005e836173a051905f5160206153025f395f51905f52910961c68052608061984061c6a05e836173c051905f5160206153025f395f51905f52910961c720526080619cc061c7405e8461c7c0526080619e4061c7e05e846173a051905f5160206153025f395f51905f52910961c860526080619ec061c8805e846173c051905f5160206153025f395f51905f52910961c9005260806191c061c9205e8761c9a052608061924061c9c05e876173a051905f5160206153025f395f51905f52910961ca405260806192c061ca605e876173c051905f5160206153025f395f51905f52910961cae052608061934061cb005e876173e051905f5160206153025f395f51905f52910961cb805260806193c061cba05e8761740051905f5160206153025f395f51905f52910961cc2052608061944061cc405e8761742051905f5160206153025f395f51905f52910961ccc05260806194c061cce05e8761744051905f5160206153025f395f51905f52910961cd6052608061954061cd805e8761746051905f5160206153025f395f51905f52910961ce005260806195c061ce205e8761748051905f5160206153025f395f51905f52910961cea052608061964061cec05e876174a051905f5160206153025f395f51905f52910961cf405260806196c061cf605e876174c051905f5160206153025f395f51905f52910961cfe0526080619a4061d0005e8561d080526080619ac061d0a05e856173a051905f5160206153025f395f51905f52910961d120526080619b4061d1405e856173c051905f5160206153025f395f51905f52910961d1c0526080619bc061d1e05e856173e051905f5160206153025f395f51905f52910961d260526080619c4061d2805e8561740051905f5160206153025f395f51905f52910961d300526080616ac061d3205e868261d3a052614886575b5f5160206153025f395f51905f5296979387809693948180808080809a8199099c60808601350999606085013509966040840135099360208301350990350808080808616e60525a601960f81b175f80a16080616b40616f005e6080612860815e805f5160206153025f395f51905f52616e605181035f0861010052614871575b806080616e806101005e61485b575b6080616b406101005e80616a805161018052614844575b8061482e575b608080616f805e5a600760f91b175f80a17c70616972696e672d62617463682d6163632d6b7a670000000000000000610100526080616f806101205e6080616f006101a05e6080616c406102205e6080616bc06102a05e5f5160206153025f395f51905f526102206101002006908115614825575b6080616c406101005e80826101805261480e575b806080616f806101805e6147f6575b80916080616bc06101005e610180526147df575b806080616f006101805e6147c7575b5a600f60f81b175f80a18015610074576147b19061504a565b505a600160fc1b175f80a1600160805260206080f35b506080616f0061010080600b5afa60803d1416614798565b50608061010060a081600c5afa60803d1416614789565b506080616f8061010080600b5afa60803d1416614775565b50608061010060a081600c5afa60803d1416614766565b60019150614752565b5060808061010081600b5afa60803d14166146dd565b50608061010060a081600c5afa60803d14166146d7565b5060808061010081600b5afa60803d14166146c0565b5060808060a081600c5afa60803d14166146b1565b9692955090925a616e806130c061a300600c608094fa3d6080141695929693919093614630565b909194606060205f5160206153025f395f51905f526001938180808c9b9a9b519b818d8c515109900899818c878c01515109900899604089015151099008970193019101612f37565b909194606060205f5160206153025f395f51905f526001938180808c9b9a9b519b818d8c515109900899818c878c01515109900899604089015151099008970193019101612e97565b9091926020805f5160206153025f395f51905f52600193818851885151099008950193019101612cdf565b5f5160206153025f395f51905f52826020600193019509926001600160801b0384168552019192612b92565b91905f5160206153025f395f51905f528086818460019509099280099201612b01565b5f5160206153025f395f51905f5260019161030051900991828160051b61a340015201906106df565b5f61a1c08201526020016106d1565b909360205f5160206153025f395f51905f528192818835865109900895019101610681565b5f5160206153025f395f51905f5260209184519008920191610671565b5f5160206153025f395f51905f5283828280602095875109880985520991019061065c565b602091815f5160206153025f395f51905f528093810387088552099101908590610604565b915f5160206153025f395f51905f52816001920992016105ed565b9181355f5160206153025f395f51905f5281101561007457815260209081019291019061059a565b90928235915f5160206153025f395f51905f528310156100745782815291815260209081019392810192910161050c565b91906080614b00838293614e8f565b938184823701910191906104cd565b91906080614b1e838293614e8f565b9381848237019101919061048d565b9190926080614b3d838293614e8f565b9381848237019101929092919261043a565b916080614b60858293969496614e8f565b948184823701910191929192610416565b916080614b82858293969496614e8f565b9481848237019101919291926103fb565b91906080614ba2838293614e8f565b938184823701910191906103a2565b614bbf608092918392614e8f565b92818582370192019190610360565b90602080918335945f5160206153025f395f51905f5286101694815201910161033e565b506080616c4060a061a1c0600c5afa60803d1416610308565b9050614c21600160381b600760386120c4615129565b9392614c37600160381b60076038612104615272565b5093919092169580614c7b575b15614c53575b505050506102f3565b909192948383178287171715151694616c4052616c6052616c8052616ca05283808080614c4a565b95838317828617171516955f616c40525f616c60525f616c80525f616ca052614c44565b915082915f616c40525f616c60525f616c80525f616ca0526102ed565b506080616bc060a061a1c0600c5afa60803d141661026a565b9050614ceb600160381b60076038612044615129565b9392614d01600160381b60076038612084615272565b5093919092169580614d45575b15614d1d575b50505050610255565b909192948383178287171715151694616bc052616be052616c0052616c205283808080614d14565b95838317828617171516955f616bc0525f616be0525f616c00525f616c2052614d0e565b915082915f616bc0525f616be0525f616c00525f616c205261024f565b801561007457602061260052602061262052602061264052612660527f73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffffff612680525f5160206153025f395f51905f526126a052602061260060c08160055afa156100745760203d03610074576126005190565b80356040820135908060801c610074578160801c610074576001600160801b038091169116906f1a0111ea397fe69a4b1ba7b6434bacd75f5160206153425f395f51905f52602085013511158183141691101715610074576f1a0111ea397fe69a4b1ba7b6434bacd75f5160206153425f395f51905f526060840135111581831416911017156100745760809060a03761012090565b81356040830135908060801c610074578160801c610074576001600160801b038091169116906f1a0111ea397fe69a4b1ba7b6434bacd75f5160206153425f395f51905f52602086013511158183141691101715610074576f1a0111ea397fe69a4b1ba7b6434bacd75f5160206153425f395f51905f52606085013511158183141691101715610074576080809282370190565b8015615047575061a1c090616cc05191616ce0925b61706084106150225783515f5160206153025f395f51905f529109801561501b57602082526020808301526020604083015260608201527f73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffffff60808201525f5160206153025f395f51905f5260a082015260208160c08160055afa60203d141692815191601f1901905b80616ce010614ff35750505f5160206153025f395f51905f5280616ce051830991616cc051900990616cc052616ce052565b5f5160206153025f395f51905f5280835185099382519009928152601f199182019101614fc1565b505f925050565b60205f5160206153025f395f51905f5281928694965190099283865201930190614f38565b90565b801561504757506080616f806103005e6101006128e06103805e6080616f006104805e6101006129e06105005e60206103008080600f5afa60203d141661030051161561007457600190565b5f96945f969293945f1901925f5b8681106150b45750505050505050565b8483820483808260051b880135921516615121575b5087858406021c16868202610100811080615100575b156150ef575b50506001016150a4565b60ff19011b9099019860015f6150e5565b9a82821b019a61010089830111156150df579b8282610100031c019b6150df565b9003836150c9565b600193925f926003820160021c845b81811061522c57505084833510156001166151e5575b93600491849582615160960294615096565b8192919381936f1a0111ea397fe69a4b1ba7b6434bacd781145f5160206153425f395f51905f5284148116806151da575b156151ca575b6f1a0111ea397fe69a4b1ba7b6434bacd7905f5160206153425f395f51905f52600160801b891095111516911017161693565b6001860195861090960195615197565b5f9750879650615191565b936151609350815f5160206153425f395f51905f526f1a0111ea397fe69a4b1ba7b6434bacd761521b8460048181988c8b615096565b92909214911416945091509361514e565b828160021b850360049081811061526a575b50026101008110615253575b50600101615138565b9760018092991b8960051b8701351016979061524a565b90505f61523e565b9290915f926001946003830160021c5f5b81811061529b575050925f9260049261516095615096565b838160021b86036004908181106152d9575b500261010081106152c2575b50600101615283565b9760018092991b8960051b850135101697906152b9565b90505f6152ad56fe4e5280109d8f96b8bfb543a6b1af25fb56a9db616af85a90eedc558e3eb1ea2973eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff000000014997c5aa3a5fa07bcaf880a9054bef831effbd9cd58e46d9bb4fb88ef99de0db64774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff000000004382d0938a760120dd6cef8f3b90a0c38abae475e3d21e39365472b76d780272 diff --git a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.sol b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.sol index 295e1067f..269e084a9 100644 --- a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.sol +++ b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.sol @@ -30,11 +30,11 @@ pragma solidity ^0.8.24; /// Keccak digest, resets the transcript buffer to that digest, then samples /// by interpreting the digest as a big-endian integer modulo r. /// - Scalar inversion uses modexp(scalar, r-2, r). -/// - Constructors run a deployment-time smoke test for the EIP-2537 +/// - Constructors run deployment-time smoke tests for MCOPY and the EIP-2537 /// precompiles using identity inputs. Compile with Solidity >=0.8.24 and /// deploy only on chains/forks that support MCOPY and EIP-2537. contract Halo2Verifier { - + /// @notice Verifying-key contract address authorized for this verifier. /// @dev The runtime length and codehash are pinned by generated constants and checked at construction time. address public immutable AUTHORIZED_VK; @@ -193,51 +193,69 @@ contract Halo2Verifier { uint256 internal constant BLS_P_MINUS_ONE_PACKED_0_WITH_ID_FLAG = 0x00000000f38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa; uint256 internal constant BLS_P_MINUS_ONE_PACKED_1 = 0x0000000000000000000000001a0111ea397fe69a4b1ba7b6434bacd764774b84; - /// @notice Smoke-check the BLS12-381 precompiles required by the verifier. - /// @dev Uses identity inputs to catch absent EIP-2537 implementations, short return data, and incompatible pairing semantics at deployment. + /// @notice Smoke-check the Cancun/EIP-2537 runtime features required by the verifier. + /// @dev Exercises MCOPY and identity EIP-2537 inputs to catch incompatible chain/fork configurations at deployment. function require_eip2537_precompiles() private view { assembly ("memory-safe") { - // Scratch is reused for every precompile probe. Start with the - // EIP-2537 identity encoding for G1/G2: all-zero padded words. + // Scratch is reused for every runtime-prerequisite probe. let scratch := 0x80 - for { let off := 0 } lt(off, 0x0180) { off := add(off, 0x20) } { + + // MCOPY must be available because the verifier uses it for + // proof-time point/scratch staging. Execute the opcode here so a + // non-Cancun fork fails during deployment instead of later proofs. + mstore(scratch, 0x1234) + mcopy(add(scratch, 0x20), scratch, 0x20) + if iszero(eq(mload(add(scratch, 0x20)), 0x1234)) { revert(0, 0) } + + // Start the EIP-2537 probes with the identity encoding for G1/G2: + // all-zero padded words. + for { let off := 0 } lt(off, 0x0300) { off := add(off, 0x20) } { mstore(add(scratch, off), 0) } // G1ADD(identity, identity) -> identity, 128-byte return. // This catches chains where the precompile is missing or returns a // non-standard success shape. - if iszero(staticcall(50000, 0x0b, scratch, 0x0100, scratch, 0x80)) { revert(0, 0) } + if iszero(staticcall(gas(), 0x0b, scratch, 0x0100, scratch, 0x80)) { revert(0, 0) } if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { revert(0, 0) } - // G1MSM([(identity, 0)]) -> identity, 128-byte return. + // Worst-case generated G1MSM with all identity/zero terms -> + // identity, 128-byte return. This exercises the largest MSM input + // length rendered by this verifier instead of only a one-pair + // smoke call. + let msm_scratch := 0xa1c0 + for { let off := 0 } lt(off, 0x30c0) { off := add(off, 0x20) } { + mstore(add(msm_scratch, off), 0) + } // The production verifier uses G1MSM both for commitments and as // the subgroup validator for absorbed proof points. - if iszero(staticcall(60000, 0x0c, scratch, 0xa0, scratch, 0x80)) { revert(0, 0) } + if iszero(staticcall(gas(), 0x0c, msm_scratch, 0x30c0, scratch, 0x80)) { revert(0, 0) } if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { revert(0, 0) } - // PAIRING_CHECK([(identity_g1, identity_g2)]) -> true, - // 32-byte return. This catches absent pairing precompiles, + // PAIRING_CHECK([(identity_g1, identity_g2), (identity_g1, identity_g2)]) + // -> true, 32-byte return. This matches the runtime two-pair KZG + // pairing input size and catches absent pairing precompiles, // short return data, and obviously incompatible semantics. - if iszero(staticcall(120000, 0x0f, scratch, 0x0180, scratch, 0x20)) { revert(0, 0) } + if iszero(staticcall(gas(), 0x0f, scratch, 0x0300, scratch, 0x20)) { revert(0, 0) } if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } if iszero(eq(mload(scratch), 1)) { revert(0, 0) } } } - + /// @notice Create a verifier pinned to a generated verifying key. - /// @dev Checks EIP-2537 availability and verifies the VK runtime before storing its address. + /// @dev Checks MCOPY/EIP-2537 availability and verifies the VK runtime before storing its address. /// @param authorizedVk Address of the generated `Halo2VerifyingKey` runtime. constructor(address authorizedVk) { // Embedded quotient path: only the external VK runtime needs to be - // pinned, but the curve precompiles are still mandatory. + // pinned, but the runtime opcode/precompile prerequisites are still + // mandatory. require_eip2537_precompiles(); require( authorizedVk.code.length == EXPECTED_VK_LENGTH @@ -270,7 +288,7 @@ contract Halo2Verifier { function verifyProof( bytes calldata proof, uint256[] calldata instances - ) external view returns (bool) { + ) external returns (bool) { // Cheap ABI-shape guard before any generated memory work: // - proof head must point at the bytes payload; // - instances head must point at the generated instance array. @@ -522,7 +540,7 @@ contract Halo2Verifier { mcopy(add(scratch, 0x80), G2_BASE_MPTR, 0x100) mcopy(add(scratch, 0x180), rhs_mptr, 0x80) mcopy(add(scratch, 0x200), NEG_S_G2_BASE_MPTR, 0x100) - ret := staticcall(170000, 0x0f, scratch, 0x0300, scratch, 0x20) + ret := staticcall(gas(), 0x0f, scratch, 0x0300, scratch, 0x20) ret := and(ret, eq(returndatasize(), 0x20)) ret := and(ret, mload(scratch)) if iszero(ret) { revert(0, 0) } @@ -801,7 +819,7 @@ contract Halo2Verifier { // Single-pair MSM output overwrites ACC_LHS_MPTR with // lhs_scalar * decoded_lhs. If lhs_scalar is one, this // is also a curve/subgroup validation round-trip. - out := staticcall(62000, 0x0c, acc_scratch, 0xa0, ACC_LHS_MPTR, 0x80) + out := staticcall(gas(), 0x0c, acc_scratch, 0xa0, ACC_LHS_MPTR, 0x80) out := and(out, eq(returndatasize(), 0x80)) } } @@ -847,7 +865,7 @@ contract Halo2Verifier { // The precompile also validates every nonzero fixed // base embedded by codegen and the carried RHS point. out := staticcall( - 62000, + gas(), 0x0c, acc_scratch, acc_msm_len, @@ -860,9 +878,23 @@ contract Halo2Verifier { // The caller checks `out` and reverts before transcript work if // any decode, canonicality, or precompile validation failed. } + + + // Section-boundary gas-attribution checkpoint. Emits a + // single LOG1 (no data) with topic = (id << 248) | gas(). + // Cost: 375 (LOG base) + 375 (1 topic) = 750 gas/call. + // Host-side parses the topic into (id, gas_left) and prints + // pairwise deltas (see `dump_gas_checkpoints`). + function gas_checkpoint(id) { + log1(0, 0, or(shl(248, id), gas())) + } + let r := FR_MODULUS let success := true + + gas_checkpoint(1) // entry: before VK loading + // =============================================================== // VK loading: either bake in the embedded VK bytes or fetch // them from the linked AUTHORIZED_VK contract. @@ -898,9 +930,18 @@ contract Halo2Verifier { // exact payload layout used by the embedded branch. extcodecopy(vk, VK_MPTR, 0x01, EXPECTED_VK_PAYLOAD_LENGTH) - // This verifier is pinned to one generated VK, so schema - // values such as instance count and accumulator layout are - // rendered as constants instead of reread from the VK header. + // Cross-check loaded VK header words against the verifier + // constants used by later parser, domain, and accumulator + // paths. Codehash pinning protects the external VK address; + // these checks catch generator drift before calldata parsing + // chooses a stale schema. + success := and(success, eq(mload(NUM_INSTANCES_MPTR), 19)) + success := and(success, eq(mload(K_MPTR), 20)) + success := and(success, eq(mload(HAS_ACCUMULATOR_MPTR), 1)) + success := and(success, eq(mload(ACC_OFFSET_MPTR), 11)) + success := and(success, eq(mload(NUM_ACC_LIMBS_MPTR), 7)) + success := and(success, eq(mload(NUM_ACC_LIMB_BITS_MPTR), 56)) + if iszero(success) { revert(0, 0) } // // The checks below validate the dynamic ABI envelope before the // transcript parser starts walking raw calldata: @@ -941,6 +982,7 @@ contract Halo2Verifier { // where the verifier converts failure to a revert. success := validate_public_accumulator(success, r) if iszero(success) { revert(0, 0) } + gas_checkpoint(2) // after VK loading + accumulator public-input precheck // =============================================================== // Transcript: VK digest + instances + proof. @@ -1009,7 +1051,9 @@ contract Halo2Verifier { // Keccak Fq transcript input. buf_len := common_word(buf_len, inst_be) } + if iszero(success) { revert(0, 0) } } + gas_checkpoint(3) // after VK digest + committed_pi + instance absorbs // =============================================================== // Per-user-phase reads + challenge squeezes. @@ -1047,6 +1091,7 @@ contract Halo2Verifier { advice_walk := add(advice_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) } + gas_checkpoint(4) // after user-phase advice reads + user challenge squeezes // ---- theta ---- // From this point onward the transcript alternates between @@ -1066,6 +1111,7 @@ contract Halo2Verifier { lookup_m_walk := add(lookup_m_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) } + gas_checkpoint(5) // after theta squeeze + lookup multiplicities // ---- beta, gamma ---- // beta and gamma are the permutation/lookup randomizers. They are @@ -1085,6 +1131,7 @@ contract Halo2Verifier { perm_z_walk := add(perm_z_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) } + gas_checkpoint(6) // after beta/gamma + permutation Z products // ---- lookup helpers + accumulators (per-lookup) ---- // Each lookup contributes zero or more helper commitments followed // by its lookup accumulator Z commitment. The generated layout keeps @@ -1125,6 +1172,7 @@ contract Halo2Verifier { calldatacopy(lookup_z_walk, proof_cptr, 0x80) lookup_z_walk := add(lookup_z_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) + gas_checkpoint(7) // after lookup helpers + Z accumulators // ---- trash_challenge ---- // Midnight squeezes this challenge unconditionally, even when the @@ -1144,6 +1192,7 @@ contract Halo2Verifier { trashcan_walk := add(trashcan_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) } + gas_checkpoint(8) // after trash_challenge + trashcans // ---- y ---- // y batches all quotient identities. Quotient commitments are read @@ -1167,6 +1216,7 @@ contract Halo2Verifier { quotient_walk := add(quotient_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) } + gas_checkpoint(9) // after y squeeze + quotient-limb reads // ---- x ---- // x is the main evaluation point. Values read after this point are @@ -1275,6 +1325,7 @@ contract Halo2Verifier { // `success` carries deferred canonicality failures from public // instance reads. G1/proof scalar helpers revert immediately. if iszero(success) { revert(0, 0) } + gas_checkpoint(10) // after evaluations + x1/x2 + f_com + x3 + q_evals + x4 + pi (transcript done) // =============================================================== // Lagrange & instance-evaluation block (pure Fr arithmetic). @@ -1354,6 +1405,7 @@ contract Halo2Verifier { mstore(L_0_MPTR, l_0) mstore(INSTANCE_EVAL_MPTR, instance_eval) } + gas_checkpoint(11) // after Lagrange + instance evaluation block if iszero(success) { revert(0, 0) } @@ -1647,7 +1699,7 @@ contract Halo2Verifier { // The default IVC verifier uses one physical encoding for the // logical VM: compact byte-oriented opcodes with variable-width // operands, dynamic runs, and limb-aware cases. - + // Byte-oriented encoding: opcodes are one byte followed by // variable-width operand bytes. for { } lt(q_pc, q_end) { } { @@ -2523,6 +2575,12 @@ contract Halo2Verifier { revert(0, 0) } } + // The VK-pinned bytecode must end exactly at q_end and every + // identity must have been consumed by a fold/native callback. + // This catches malformed generator output whose final opcode + // over-reads operands or leaves a partial expression live. + if iszero(eq(q_pc, q_end)) { revert(0, 0) } + if q_has_top { revert(0, 0) } // Structured post-VM suffix. The current default uses this for // regular trash constraints: it is smaller than fully unrolled @@ -2779,6 +2837,7 @@ contract Halo2Verifier { mstore(QUOTIENT_EVAL_MPTR, linearization_expected_eval) pop(y) } + gas_checkpoint(12) // after batched identity numerator reconstruction // =============================================================== // Prepare linearization scalars for the final PCS MSM. @@ -2820,6 +2879,7 @@ contract Halo2Verifier { mstore(QUOTIENT_MPTR, x_split) mstore(add(QUOTIENT_MPTR, 0x20), one_minus_x_n) } + gas_checkpoint(13) // after linearization scalar prep // =============================================================== // PCS computation (multi-prepare emitter from Step 5). @@ -2834,6 +2894,7 @@ contract Halo2Verifier { { // Generated PCS sub-block 1. These lines are // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. { // 4 distinct rotation(s) let x := mload(X_MPTR) @@ -2857,8 +2918,10 @@ contract Halo2Verifier { x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) mstore(add(ROT_POINTS_MPTR, 0x0), x_pow_of_omega) } + gas_checkpoint(17) // after PCS sub-block 1 // Generated PCS sub-block 2. These lines are // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. { // pre-compute 43 x1 power(s) let x1 := mload(X1_MPTR) @@ -2871,8 +2934,10 @@ contract Halo2Verifier { mstore(p, and(acc, 0xffffffffffffffffffffffffffffffff)) } } + gas_checkpoint(18) // after PCS sub-block 2 // Generated PCS sub-block 3. These lines are // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. { // q_eval_set[0]: 43 commitment(s) (rolled, m>=4) // stage per-(commit, rotation) eval source addresses @@ -2930,8 +2995,10 @@ contract Halo2Verifier { } mstore(add(Q_EVAL_SET_MPTR, 0x0), q_eval_set_0) } + gas_checkpoint(19) // after PCS sub-block 3 // Generated PCS sub-block 4. These lines are // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. { // q_eval_set[1]: 3 commitment(s) let q_eval_set_0 := mload(0x86e0) @@ -2943,8 +3010,10 @@ contract Halo2Verifier { mstore(add(Q_EVAL_SET_MPTR, 0x20), q_eval_set_0) mstore(add(Q_EVAL_SET_MPTR, 0x40), q_eval_set_1) } + gas_checkpoint(20) // after PCS sub-block 4 // Generated PCS sub-block 5. These lines are // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. { // q_eval_set[2]: 3 commitment(s) let q_eval_set_0 := mload(0x9060) @@ -2956,8 +3025,10 @@ contract Halo2Verifier { mstore(add(Q_EVAL_SET_MPTR, 0x60), q_eval_set_0) mstore(add(Q_EVAL_SET_MPTR, 0x80), q_eval_set_1) } + gas_checkpoint(21) // after PCS sub-block 5 // Generated PCS sub-block 6. These lines are // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. { // q_eval_set[3]: 11 commitment(s) (rolled, m>=4) // stage per-(commit, rotation) eval source addresses @@ -3011,8 +3082,10 @@ contract Halo2Verifier { mstore(add(Q_EVAL_SET_MPTR, 0xc0), q_eval_set_1) mstore(add(Q_EVAL_SET_MPTR, 0xe0), q_eval_set_2) } + gas_checkpoint(22) // after PCS sub-block 6 // Generated PCS sub-block 7. These lines are // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. { // q_eval_set[4]: 5 commitment(s) (rolled, m>=4) // stage per-(commit, rotation) eval source addresses @@ -3048,8 +3121,10 @@ contract Halo2Verifier { mstore(add(Q_EVAL_SET_MPTR, 0x120), q_eval_set_1) mstore(add(Q_EVAL_SET_MPTR, 0x140), q_eval_set_2) } + gas_checkpoint(23) // after PCS sub-block 7 // Generated PCS sub-block 8. These lines are // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. { // f_eval via Horner over 5 reversed set(s) let x2 := mload(X2_MPTR) @@ -3215,8 +3290,10 @@ contract Halo2Verifier { } mstore(F_EVAL_MPTR, f_eval) } + gas_checkpoint(24) // after PCS sub-block 8 // Generated PCS sub-block 9. These lines are // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. { // build final_com and v (KZG single-opening proof, fused MSM) // final MSM input length from circuit/VK shape: 78 term(s) @@ -3403,13 +3480,15 @@ contract Halo2Verifier { mcopy(0xd320, F_COM_MPTR, 0x80) mstore(0xd3a0, x4_pow_5) if success { - success := staticcall(575096, 0x0c, 0xa300, 0x30c0, FINAL_COM_MPTR, 0x80) + success := staticcall(gas(), 0x0c, 0xa300, 0x30c0, FINAL_COM_MPTR, 0x80) success := and(success, eq(returndatasize(), 0x80)) } mstore(V_MPTR, v) } + gas_checkpoint(25) // after PCS sub-block 9 // Generated PCS sub-block 10. These lines are // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. { // Scale z*pi - vG before the final pairing check // pairing inputs (LHS = pi; RHS = final_com - v*G + x3*pi) @@ -3417,27 +3496,28 @@ contract Halo2Verifier { mcopy(0x80, G1_BASE_MPTR, 0x80) mstore(0x100, addmod(0, sub(r, mload(V_MPTR)), r)) if success { - success := staticcall(62000, 0x0c, 0x80, 0xa0, 0x80, 0x80) + success := staticcall(gas(), 0x0c, 0x80, 0xa0, 0x80, 0x80) success := and(success, eq(returndatasize(), 0x80)) } mcopy(0x100, FINAL_COM_MPTR, 0x80) if success { - success := staticcall(50000, 0x0b, 0x80, 0x100, 0x80, 0x80) + success := staticcall(gas(), 0x0b, 0x80, 0x100, 0x80, 0x80) success := and(success, eq(returndatasize(), 0x80)) } mcopy(0x100, PI_MPTR, 0x80) mstore(0x180, mload(X3_MPTR)) if success { - success := staticcall(62000, 0x0c, 0x100, 0xa0, 0x100, 0x80) + success := staticcall(gas(), 0x0c, 0x100, 0xa0, 0x100, 0x80) success := and(success, eq(returndatasize(), 0x80)) } if success { - success := staticcall(50000, 0x0b, 0x80, 0x100, 0x80, 0x80) + success := staticcall(gas(), 0x0b, 0x80, 0x100, 0x80, 0x80) success := and(success, eq(returndatasize(), 0x80)) } mcopy(PAIRING_RHS_MPTR, 0x80, 0x80) } } + gas_checkpoint(14) // after PCS computation block (= sub-block 6) // Batch the prevalidated public IVC accumulator pairing equation // into the final KZG pairing. @@ -3473,12 +3553,12 @@ contract Halo2Verifier { mcopy(batch_ptr, ACC_RHS_MPTR, 0x80) mstore(add(batch_ptr, 0x80), acc_pair_alpha) if success { - success := staticcall(62000, 0x0c, batch_ptr, 0xa0, batch_ptr, 0x80) + success := staticcall(gas(), 0x0c, batch_ptr, 0xa0, batch_ptr, 0x80) success := and(success, eq(returndatasize(), 0x80)) } mcopy(add(batch_ptr, 0x80), PAIRING_RHS_MPTR, 0x80) if success { - success := staticcall(50000, 0x0b, batch_ptr, 0x0100, PAIRING_RHS_MPTR, 0x80) + success := staticcall(gas(), 0x0b, batch_ptr, 0x0100, PAIRING_RHS_MPTR, 0x80) success := and(success, eq(returndatasize(), 0x80)) } @@ -3487,15 +3567,16 @@ contract Halo2Verifier { mcopy(batch_ptr, ACC_LHS_MPTR, 0x80) mstore(add(batch_ptr, 0x80), acc_pair_alpha) if success { - success := staticcall(62000, 0x0c, batch_ptr, 0xa0, batch_ptr, 0x80) + success := staticcall(gas(), 0x0c, batch_ptr, 0xa0, batch_ptr, 0x80) success := and(success, eq(returndatasize(), 0x80)) } mcopy(add(batch_ptr, 0x80), PAIRING_LHS_MPTR, 0x80) if success { - success := staticcall(50000, 0x0b, batch_ptr, 0x0100, PAIRING_LHS_MPTR, 0x80) + success := staticcall(gas(), 0x0b, batch_ptr, 0x0100, PAIRING_LHS_MPTR, 0x80) success := and(success, eq(returndatasize(), 0x80)) } } + gas_checkpoint(15) // after public accumulator pairing batch prep (omitted for no-accumulator VKs) // The Yul `ec_pairing` helper checks // e(arg0, G2_BASE) * e(arg1, NEG_S_G2_BASE) == 1 @@ -3512,8 +3593,9 @@ contract Halo2Verifier { // pairing argument order. Pass them swapped to ec_pairing. if iszero(success) { revert(0, 0) } success := ec_pairing(success, PAIRING_RHS_MPTR, PAIRING_LHS_MPTR) + gas_checkpoint(16) // after final ec_pairing + - // Success path is terminal. Invalid inputs have already reverted, // so the Solidity ABI observes `true`. diff --git a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md index 9b4c21b25..4a2996664 100644 --- a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md +++ b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md @@ -8,23 +8,31 @@ chain. | Contract | Address | | --- | --- | -| `Halo2VerifyingKey` | `0x2fE3dB07Cc00f6dFF002C37d5A17ebfAd7aA88D0` | -| `Halo2Verifier` | `0x049510AfEC69eD54409dabB57195FfD38400F4D0` | +| `Halo2VerifyingKey` | [`0x0D2789bcDF4A0406Dc7383BD7D4b73A7f08CaC16`](https://sepolia.etherscan.io/address/0x0D2789bcDF4A0406Dc7383BD7D4b73A7f08CaC16) | +| `Halo2Verifier` | [`0xA826f6e66567EAFCd618175041a8F9E79f4661a2`](https://sepolia.etherscan.io/address/0xA826f6e66567EAFCd618175041a8F9E79f4661a2) | ## Transactions | Action | Transaction | | --- | --- | -| Deploy VK | `0xd6e7c6180a57c424327df03fe5b4ed0a274c73a4e8f681913582db5cf9e809cf` | -| Deploy verifier | `0x2236dc333064a88e7f23fc669570f72bf52163ca1cf637c29a6e77b743bfa60e` | -| Verify fresh proof | `0xd536e9162bab92c71b7b9f1bb94f00def8b50e441144c6aebd7708a51400abfb` | +| Deploy VK | [`0x22db64c0b4a15460d93b3091359f7988123be59b945cec4bcb91b3498587f588`](https://sepolia.etherscan.io/tx/0x22db64c0b4a15460d93b3091359f7988123be59b945cec4bcb91b3498587f588) | +| Deploy verifier | [`0x5bb4769deb461e2ca68a8ebaf1fbf44a0dbe9c526e4d7795b394a0292a853882`](https://sepolia.etherscan.io/tx/0x5bb4769deb461e2ca68a8ebaf1fbf44a0dbe9c526e4d7795b394a0292a853882) | +| Verify fresh proof | [`0x696e5b99fbdfc31adc71046d2afd01897a20f05202a46c2f0e70614853e9ac90`](https://sepolia.etherscan.io/tx/0x696e5b99fbdfc31adc71046d2afd01897a20f05202a46c2f0e70614853e9ac90) | + +## Gas + +| Action | Gas used | +| --- | ---: | +| Deploy VK | `3,723,458` | +| Deploy verifier | `5,349,137` | +| Verify fresh proof | `1,310,991` | ## Runtime Metadata | Contract | Runtime bytes | Runtime code hash | | --- | ---: | --- | | `Halo2VerifyingKey` | `17025` | `0x24430c63ec2017f12ec4004d66c66314f418dd77fe38dd4c2475c8f2b259e009` | -| `Halo2Verifier` | `21119` | `0x1422a1464cc070cd2d88cc9468edf6d88d99ead6cacbd942bfb0257e209f3b10` | +| `Halo2Verifier` | `21410` | `0x2bebc2f2ae0446f93f97cda1a72c5b6b58b9aead3a3eaf3b12927ae1bd92d981` | ## Files diff --git a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/deployment.json b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/deployment.json index b9145d3b1..13d43c119 100644 --- a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/deployment.json +++ b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/deployment.json @@ -4,8 +4,8 @@ "deployer": "0x609A4Ff8347Bb82d65a12A597298F256Fd4DE493", "contracts": { "Halo2VerifyingKey": { - "address": "0x2fE3dB07Cc00f6dFF002C37d5A17ebfAd7aA88D0", - "deploymentTx": "0xd6e7c6180a57c424327df03fe5b4ed0a274c73a4e8f681913582db5cf9e809cf", + "address": "0x0D2789bcDF4A0406Dc7383BD7D4b73A7f08CaC16", + "deploymentTx": "0x22db64c0b4a15460d93b3091359f7988123be59b945cec4bcb91b3498587f588", "gasUsed": 3723458, "runtimeBytes": 17025, "runtimeCodeHash": "0x24430c63ec2017f12ec4004d66c66314f418dd77fe38dd4c2475c8f2b259e009", @@ -13,18 +13,18 @@ "sourceFile": "Halo2VerifyingKey.sol" }, "Halo2Verifier": { - "address": "0x049510AfEC69eD54409dabB57195FfD38400F4D0", - "deploymentTx": "0x2236dc333064a88e7f23fc669570f72bf52163ca1cf637c29a6e77b743bfa60e", - "gasUsed": 4709970, - "runtimeBytes": 21119, - "runtimeCodeHash": "0x1422a1464cc070cd2d88cc9468edf6d88d99ead6cacbd942bfb0257e209f3b10", + "address": "0xA826f6e66567EAFCd618175041a8F9E79f4661a2", + "deploymentTx": "0x5bb4769deb461e2ca68a8ebaf1fbf44a0dbe9c526e4d7795b394a0292a853882", + "gasUsed": 5349137, + "runtimeBytes": 21410, + "runtimeCodeHash": "0x2bebc2f2ae0446f93f97cda1a72c5b6b58b9aead3a3eaf3b12927ae1bd92d981", "runtimeBytecodeFile": "Halo2Verifier.runtime.bytecode.hex", "sourceFile": "Halo2Verifier.sol" } }, "proofVerificationTx": { - "transactionHash": "0xd536e9162bab92c71b7b9f1bb94f00def8b50e441144c6aebd7708a51400abfb", - "gasUsed": 1291664, + "transactionHash": "0x696e5b99fbdfc31adc71046d2afd01897a20f05202a46c2f0e70614853e9ac90", + "gasUsed": 1310991, "status": 1 }, "compiler": { From 4bab29c9e5aa5b8ec3aa512c17cdfa998e6e8900 Mon Sep 17 00:00:00 2001 From: Julien Coolen Date: Tue, 19 May 2026 11:58:29 +0100 Subject: [PATCH 16/19] Deploy Moonlight Sepolia verifier without gas checkpoints --- .../Halo2Verifier.runtime.bytecode.hex | 2 +- .../sepolia/moonlight-wrap/Halo2Verifier.sol | 35 +------------------ .../sepolia/moonlight-wrap/README.md | 19 +++++----- .../sepolia/moonlight-wrap/deployment.json | 22 +++++++----- 4 files changed, 26 insertions(+), 52 deletions(-) diff --git a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.runtime.bytecode.hex b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.runtime.bytecode.hex index 2ba2ddc38..97584913b 100644 --- a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.runtime.bytecode.hex +++ b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.runtime.bytecode.hex @@ -1 +1 @@ -0x6108e06040526004361015610012575f80fd5b5f3560e01c80631e8e1e1314610078576342b7259714610030575f80fd5b34610074575f366003190112610074576040517f0000000000000000000000000d2789bcdf4a0406dc7383bd7d4b73a7f08cac166001600160a01b03168152602090f35b5f80fd5b346100745760403660031901126100745760043567ffffffffffffffff8111610074573660238201121561007457806004013567ffffffffffffffff81116100745760243691830101116100745760243567ffffffffffffffff8111610074573660238201121561007457806004013567ffffffffffffffff8111610074576024369160051b8301011161007457611ec060409114911416156100745760017f0000000000000000000000000d2789bcdf4a0406dc7383bd7d4b73a7f08cac165a8260f81b175f80a17f24430c63ec2017f12ec4004d66c66314f418dd77fe38dd4c2475c8f2b259e009813f14614281823b1416156100745781612700614280923c6013612720511481166014612740511416816127e0511416600b61280051141660076128205114166038612840511416801561007457611e6060443514166013611ec43514163661214414168015610074575f90731a0111ea397fe69a4b1ba7b6434bacd764774b846120a435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa612084351416731a0111ea397fe69a4b1ba7b6434bacd764774b8461206435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa6120443514161680614d69575b15614cd5575b166080616bc061a1c05e808261a24052614cbc575b5f90731a0111ea397fe69a4b1ba7b6434bacd764774b8461212435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa612104351416731a0111ea397fe69a4b1ba7b6434bacd764774b846120e435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa6120c43514161680614c9f575b15614c0b575b1680916080616c4061a1c05e61a24052614bf2575b8015610074575a600160f91b175f80a160806127005190525f60a0525f60c0525f60e0525f61010052601361012052610140611ee45b6121448110614bce57508115610074575a600360f81b175f80a16064906191c0905b826107e48110614bb1575f5160206153025f395f51905f5285925a600160fa1b175f80a1607f190160802080608052066169805260a090619940916101008201925b838310614b935784835f5160206153025f395f51905f52845a600560f81b175f80a1607f190160802080608052066169a0525f5160206153025f395f51905f52602060802080608052066169c05260a0619a4061030083015b808410614b715750505a600360f91b175f80a1619d40608083015b808410614b4f5750610100608061042e858095614e8f565b948181619e4037019201905b818310614b2d5750505f5160206153025f395f51905f52608061045e838095614e8f565b928181619ec03701915a600760f81b175f80a1607f190160802080608052066169e05260a0610100619f409301925b838310614b0f5784835f5160206153025f395f51905f52845a600160fb1b175f80a1607f19016080208060805206616a005260a090619fc0916102008201925b838310614af15784835f5160206153025f395f51905f52845a600960f81b175f80a1607f19016080208060805206616a205260a090618500610cc08201905b818310614ac05750505f5160206153025f395f51905f528192607f19016080208060805206616a40525f5160206153025f395f51905f5260206080208060805206616a6052610120608061055f83614df9565b928181616ac0370191607f190160802092836080526001600160801b035f5160206153025f395f51905f5260a0950616616a8052826182a052015b808210614a9857506080905f5160206153025f395f51905f52611ec493607f1901832080845206616aa0526105ce81614df9565b508181616b40370103610074575a600560f91b175f80a1616a205180915f5b60148110614a7d57506127805192616cc0846127c0515b6170608310614a58575050505061063d5f5160206153025f395f51905f525f5160206153625f395f51905f528408918261706052614f23565b925f5160206153025f395f51905f52616cc092612760519009916127c0515b6170608210614a33578585616ce05190616d00915b616e008310614a16575f92611ee4905b61214482106149f157505061706051616cc05190616e005193616cc052616ce052616d0052616d2052616d4052616d60525a600b60f81b175f80a1801561007457616a0051610300525f61a300525f5b61014081106149e257506001805b603182106149b95782618b4051618a40516185205190618a6051916185405161088052618a8051936185605194618aa0516108c0526185805190618ac0516185a0516108a052618ae05191886185c0519586946108805189618b0051905f5160206153025f395f51905f529109905f5160206153025f395f51905f529109955f5160206153025f395f51905f529109936108a0515f5160206153025f395f51905f52910992866108c051905f5160206153025f395f51905f529109925f5160206153025f395f51905f52910990610880515f5160206153025f395f51905f52908c09905f5160206153025f395f51905f528a8c095f5160206153025f395f51905f529108905f5160206153025f395f51905f529108905f5160206153025f395f51905f529108905f5160206153025f395f51905f529108905f5160206153025f395f51905f529108905f5160206153025f395f51905f529108905f5160206153025f395f51905f5291088684618b2051905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f5291085f5160206153025f395f51905f5290600109956103005161a30051905f5160206153025f395f51905f5291099661a1c051905f5160206153025f395f51905f52910861a1c0526108a0515f5160206153025f395f51905f52035f5160206153025f395f51905f52905f08915f5160206153025f395f51905f52035f5160206153025f395f51905f52905f089061088051905f5160206153025f395f51905f529108905f5160206153025f395f51905f529108905f5160206153025f395f51905f5291085f5160206153025f395f51905f529060010961a1e051905f5160206153025f395f51905f52910861a1e0525f5160206153025f395f51905f52035f5160206153025f395f51905f52905f08915f5160206153025f395f51905f529108905f5160206153025f395f51905f5291085f5160206153025f395f51905f529060010961a20051905f5160206153025f395f51905f529108906185e0515f5160206153025f395f51905f52035f5160206153025f395f51905f52905f089061088051905f5160206153025f395f51905f529108905f5160206153025f395f51905f5291085f5160206153025f395f51905f5290600109918261a9605261030051906103005190610300515f5160206153025f395f51905f529109905f5160206153025f395f51905f529109905f5160206153025f395f51905f52910961a3005261a360515f5160206153025f395f51905f529109905f5160206153025f395f51905f52910861a20052614120905f9061a960825b84906152ef861015611f7057600186515f1a9601959182600514611f44575081600614611f245781600814611f095781600d14611edf5781601014611ebb5781601114611e975781602114611bf85750806019146118ad5780601f146113575780601b14610b9857600b14610b27575f80fd5b600384519401935f905f5160206153025f395f51905f526103005161a300510961a300525f5160206153025f395f51905f5285611fe08360f31c1661a1c0019283519061ffff8160e81c16610b80575b50089052610ab4565b90621fffe0849260e31c1661a3400151900989610b77565b505082516002909301925f925061a96090839060f01c8015610ec45780600114610d985780600214610cb357600314610bcf575f80fd5b5f5160206153025f395f51905f528080808080618b0051816185e05181035f0890088180618520518161858051918009097f404d21073985d14e432a4ad76d3fae06ca74314b950fe7b1d7f501cd31a8b374099008818061854051816185a051918009097f0b2cc8704264c6bd81bc620e9e524d4b73e9b2317679422ff7fa1603955649f10990088180618560518161862051918009097f0fdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f099008600109808452816103005161a300510961a300528161a2e05161a3605190090861a2e052610ab4565b505f5160206153025f395f51905f528080808080618ae051816185c05181035f0890088180618520518161858051918009097f1b8114c381b922fd5d6d241210e2d8a68ad5744053ba9e776118de4107b51ace099008818061854051816185a051918009097f3df32e4cc4cb2ed20e5d21899cf5331775990ccaec4c09b4e3717213fcc0d7630990088180618560518161862051918009097f3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc1099008600109808452816103005161a300510961a300528161a2e05161a3605190090861a2e052610ab4565b505f5160206153025f395f51905f528080807f73eda753299d7d483339d80809a1d7f7b67900f7fe6bfad98998021b7bb5732b81807f73eda753299d7d483339d80809a1d80553bca402fffe5bfeffffffff0000000181808080600160701b61852051088180600160701b6185405108600160381b0990088180600160701b6185605108600160701b099008818080618660518161868051600160381b0990086186a0518290600160701b09900881035f08900808818070241eabfffeb153ffffb9feffffffffaaab6187c0510981035f089008088180600160861b817f73eda753299d7d483339d80809a1d80553bda402fffe5b6e855000003ab000026187e051080981035f089008600109808452816103005161a300510961a3005261a240510861a24052610ab4565b5061852051610400526185c051610520526185e051610480526186005161044052618540516104a0526187a0516104c0525f5160206153025f395f51905f528080807f73eda753299d7d483339d80809a1d7d340dd972492de594de627fffefcb803498180618560516187805161054052618580516105805261876051610460526185a0516104205261874051610500526186205161056052618640516104e05281808080618660518161868051600160381b0990086186a0518290600160701b09900881035f089181808061044051600160701b09818061048051600160381b096105205108089181808083600160701b0981806104a051600160381b09610400510808918180806104c0516104e05109700c8557e86f90d0d89eed6eb5349a0f88200991818080610540516104e05109702cb9b546d20373eaf85e8f53db883cb5480991818080610460516104e0510970013af65741744bd7bb2c6872df2b8003200991818080610500516104e0510970340f2ebe380a0f5eff4360543988a61dc20991818080610440516104e0510970297784894e27525bc342b7fde37dba93660991818080610480516104e05109703212e00cde6d2002b119d800000347fcb809918180806104c0516105605109702cb9b546d20373eaf85e8f53db883cb548099181808061054051610560510970013af65741744bd7bb2c6872df2b800320099181808061046051610560510970340f2ebe380a0f5eff4360543988a61dc2099181808061050051610560510970297784894e27525bc342b7fde37dba93660991818080610440516105605109703212e00cde6d2002b119d800000347fcb809918180806104c051610420510970013af65741744bd7bb2c6872df2b800320099181808061054051610420510970340f2ebe380a0f5eff4360543988a61dc2099181808061046051610420510970297784894e27525bc342b7fde37dba93660991818080610500516104205109703212e00cde6d2002b119d800000347fcb809918180806104c051610580510970340f2ebe380a0f5eff4360543988a61dc2099181808061054051610580510970297784894e27525bc342b7fde37dba93660991818080610460516105805109703212e00cde6d2002b119d800000347fcb809918180806104c051840970297784894e27525bc342b7fde37dba936609918180808080610540518609703212e00cde6d2002b119d800000347fcb80993610520519009600160701b098180806104c0516104a05109703212e00cde6d2002b119d800000347fcb809818080610480516104a05109600160701b09818080610520516104a05109600160381b09818080610440516104005109600160701b09818080610480516104005109600160381b09816105205161040051090808080808080808080808080808080808080808080808080808080808818070241eabfffeb153ffffb9feffffffffaaab6187c0510981035f089008088180600160861b817f73eda753299d7d483339d80809a1d80553b9202d7ffe85d4800008bb200000016187e051080981035f089008600109808452816103005161a300510961a3005261a220510861a22052610ab4565b505090505f905f61a96090616d40516103a052616d005161032052616d20516103e0526169a05161038052616980516103c0525f5160206153025f395f51905f52806190e05181610320516103a0510809816103005161a30051090861a300525f5160206153025f395f51905f52618b6051816103c05191816103c0515f0908095f5b6004811061188057505060015f5b6004811061185e5750600161a9e05260015b6004811061182c5750600161aac0526003805b6117fa57505f905f5b600481106117cd5750905f5160206153025f395f51905f52808080808096816190c05197810391880908816103005161a3005109089381808080618be051948180618b8051816103c0515f090881618ba051916103c05190090895096190e0510881036191005108816190a0519361038051900890090881806103e05161032051088103600108099161030051900908610360526191605161034052618a00516102e05261852051610260526185405161028052618560516102c052618580516102a0526185a0516102405261862051610220526186405161020052618660516101e052618680516101c0526186a0516101a0526186c051610180526186e0516101605261870051610140526187205161012052618bc051610100526191405160e0525f5160206153025f395f51905f5280618c005181035f0860010860c0525f5160206153025f395f51905f52808060e05160010961034051088103619180510860a0525f5160206153025f395f51905f5280806191205181806103805181806101005160c05109816103c05181806101205160c05109816103c05181806101405160c05109816103c05181806101605160c05109816103c05181806101805160c05109816103c05181806101a05160c05109816103c05181806101c05160c05109816103c05181806101e05160c05109816103c05181806102005160c05109816103c05181806102205160c05109816103c05181806102405160c05109816103c05181806102a05160c05109816103c05181806102c05160c05109816103c05181806102805160c05109816103c05181806102605160c05109816103c05181806102e05160c05109816103c0515f09080908090809080908090809080908090809080908090809080908090809080860a051090881806103e0516103205108810360010809816103005181805f5160206153625f395f51905f528180610380518161010051816103c0518161012051816103c0518161014051816103c0518161016051816103c0518161018051816103c051816101a051816103c051816101c051816103c051816101e051816103c0518161020051816103c0518161022051816103c0518161024051816103c051816102a051816103c051816102c051816103c0518161028051816103c0518161026051816103c051816102e051816103c0515f09080908090809080908090809080908090809080908090809080908090809080860e0510908816103005181806103405181610320516103a051080981610300516103605109080908090861a30052610ab4565b915f5160206153025f395f51905f52600191818560051b8061aa6001519061a9e001510990089201611416565b5f5160206153025f395f51905f528160051b808601519061aa600151095f19820160051b61aa6001525f19018061140d565b6001905f5160206153025f395f51905f525f19820160051b808701519061a9e00151098160051b61a9e00152016113fa565b905f5160206153025f395f51905f526001918360051b860151900991016113e8565b8060019160051b5f5160206153025f395f51905f52610380518183618540015187080890860152016113da565b5050618a205161a9609081525f925082805b60058110611bdf57506185005161aa2052616d605161aa40525f5b60098110611bc65750618a005161ab80525f5b60128110611bad57505f5b60068110611b9257505f5b60068110611b7757505f5b60058110611b5c57505f5160206153025f395f51905f5280808061ade0518103600108616d405109816103005161a30051090881808061ae80518181810391800908616d005109916103005190090861a3005260015b60068110611b1657505f5160206153025f395f51905f52616a20516169a0510961b000525f5b600681106119985750610ab4565b600381026003810160128111611b0e575b8260051b908161aea001519161ade001519061b00051908194906169c051906169a0515b8a828510611a4157505050505050600193925f5160206153025f395f51905f52808080957f4285088329c399ea457a8ca1d30f8957e74c7f529842a1579b4fee55b398292395820390088180616d2051616d0051088103890809816103005161a30051090861a300520961b000520161198a565b9285979385969792939482809760051b809301519261aba001515f5160206153025f395f51905f529087095f5160206153025f395f51905f52908408905f5160206153025f395f51905f5291085f5160206153025f395f51905f529109985f5160206153025f395f51905f529108905f5160206153025f395f51905f5291085f5160206153025f395f51905f529109947f08634d0aa021aaf843cab354fabb0062f6502437c6a09c006c083479590189d75f5160206153025f395f51905f529109936001019291906119cd565b5060126119a9565b805f5160206153025f395f51905f52808060019460051b61ade001515f19850160051b61af60015182039008616d405109816103005161a30051090861a3005201611964565b80606060019202618ec001518160051b61af6001520161190e565b80606060019202618ea001518160051b61aea0015201611903565b80606060019202618e8001518160051b61ade00152016118f8565b8060019160051b80618c4001519061aba00152016118ed565b8060019160051b8061862001519061aa600152016118da565b8060019160051b8061852001519061a9800152016118bf565b92939480915090600181515f1a91015f9260018316611e87575b505f9360028316611e6e575b815196875f1a8860011a908960021a9a60058b60041a960199611e60575b505f5b818110611e135750505f5b818110611db45750505f5b878a821015611cec5788519860118a60f01c9101995f5b60078110611c805750505050600101611c55565b8060051b8301515f6080525b600760805110611c9f5750600101611c6c565b9a5f5160206153025f395f51905f5290818d81600460805187018a0101515f1a60051b612ae001519160805160051b61ffff8960e01c16015190090990089a600160805101608052611c8c565b5050959750955f905b8060031a8210611d7a5750505f905b808210611d36575050600116611d1e575b50916001610ab4565b905f5160206153025f395f51905f5291510984611d15565b90935f5160206153025f395f51905f526001918160058b519b019a81815f1a60051b612ae001519161ffff808260d81c16519160e81c165109099008940190611d04565b90945f5160206153025f395f51905f526001918160038c519c019b61ffff8160e81c1651905f1a60051b612ae00151099008950190611cf5565b6002895160f01c990198515f905b8a60078310611dd657505050600101611c4a565b600191929a5f5160206153025f395f51905f5260038193519e019d8161ffff825f1a60051b612ae001519260e81c16518709099008990190611dc2565b5f5b8a60078210611e28575050600101611c3f565b6001919a5f5160206153025f395f51905f5260038193519e019d61ffff8160e81c1651905f1a60051b612ae001510990089901611e15565b84526020909301928b611c3c565b9350600181515f1a91019060051b612ae0015193611c1e565b905160f01c925060030187611c12565b935f5160206153025f395f51905f5291506002865160f01c96019551900992610ab4565b935f5160206153025f395f51905f5291506002865160f01c96019551900892610ab4565b935f5160206153025f395f51905f529150600186515f1a96019560051b612ae00151900992610ab4565b935f5160206153025f395f51905f52809250035f0892610ab4565b93915f5160206153025f395f51905f529150601f19019182510892610ab4565b92949150946003905160f01c920194611f62575b5051916001610ab4565b835260209092019184611f58565b83906152ef870361007457610074576169e051610840525f5160206153025f395f51905f52618ae051815f5160206153625f395f51905f526185c051099008610720526185205161082052618540516108005261856051610760525f5160206153025f395f51905f526107605161076051096107e05261858051610860525f5160206153025f395f51905f52610860516108605109610740526185a0516107a0525f5160206153025f395f51905f526107a0516107a051096107805261862051610700525f5160206153025f395f51905f526107005161070051096107c052618640516106e0525f5160206153025f395f51905f526106e0516106e051096106c052618660516106a0525f5160206153025f395f51905f526106a0516106a05109610680525f5160206153025f395f51905f52618b0051815f5160206153625f395f51905f526185e05109900861066052618b205161064052618b405161062052618a405161060052618a60516105e052618a80516105c0525f5160206153025f395f51905f52618aa051815f5160206153625f395f51905f52618600510990086105a0525f5160206153025f395f51905f5280808080618c205181036001086191a0519009810381808080806106805161068051096106a051095f5160206153225f395f51905f5209818080806106c0516106c051096106e051095f5160206153825f395f51905f5209818080806107c0516107c0510961070051095f5160206152e25f395f51905f5209818080806107805161078051096107a051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e098180808061074051610740510961086051097ed94c46a8456352aa44d7a885ab59e3a36664e6fb25e826f8a4cd79822f053309818080806107e0516107e0510961076051097f1f61345b652161410c5e29f51e301ae56342af824bc110649393d2b911c50d3e098180610800517f40fa389feb2522bb934881ac9ed749aee2296502af592418c6b5675c0f560261098180610820517f70d8f2a733a64d650faccc9b1c2a766a9544bb3ff1a11ee73cb43947ef386633096105a0510808080808080808816108405181808080806106c0516106c051096106e051095f5160206153225f395f51905f5209818080806107c0516107c0510961070051095f5160206153825f395f51905f5209818080806107805161078051096107a051095f5160206152e25f395f51905f52098180808061074051610740510961086051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e09818080806107e0516107e0510961076051097ed94c46a8456352aa44d7a885ab59e3a36664e6fb25e826f8a4cd79822f0533098180610800517f27e7119226c42a6d19c1541904b99ae40685511ed2e078964b74594d38340849098180610820517f6d05a41959f539a7fc9ec0972ea1e3dbb6fc67dd51daf3414f7fbbb091c7274a0981805f5160206153625f395f51905f526106a051096105c0510808080808080808816108405181808080806107c0516107c0510961070051095f5160206153225f395f51905f5209818080806107805161078051096107a051095f5160206153825f395f51905f52098180808061074051610740510961086051095f5160206152e25f395f51905f5209818080806107e0516107e0510961076051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e098180610800517f23a6684b942d726a22e4d5b8d8ff83aeaa773f62600184efe5d033d7c7c6e827098180610820517f2f5908b169c6cf1bd26dcf0f9e5105481f5164f3ece0582bf3098312167751a70981805f5160206153625f395f51905f526106e051096105e05108080808080808816108405181808080806107805161078051096107a051095f5160206153225f395f51905f52098180808061074051610740510961086051095f5160206153825f395f51905f5209818080806107e0516107e0510961076051095f5160206152e25f395f51905f52098180610800517f24822e1af9aa2887c912c87eb0f20bd332330e7e55cd784de67cb407a9f05520098180610820517f726df1506749848155630b86ae25a82b281ecd050fe3a52d85a181fa87202e4b0981805f5160206153625f395f51905f526107005109610600510808080808088161084051818080808061074051610740510961086051095f5160206153225f395f51905f5209818080806107e0516107e0510961076051095f5160206153825f395f51905f52098180610800517f26c2cc87f95726b28f33ca03409a460ec987cfe12adae32769e3565865d07191098180610820517f222e83e70453dfee19b402e9fa8dfe2c4987b034d0be3ceb478b3022e97934c10981805f5160206153625f395f51905f526107a05109610620510808080808816108405181808080806107e0516107e0510961076051095f5160206153225f395f51905f52098180610800517f6bd72f9cfc53af9d931896e77ea5c61244cb6d5fae8954f37dc7b9002f5aa78a098180610820517f5e1d3dbecda6214343e24a47f45c5d033197ad01b65a730af95dc57e90c491400981805f5160206153625f395f51905f5261086051096106405108080808816108405181808080806106805161068051096106a051097f0fdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f09818080806106c0516106c051096106e051097f301cf56f9b4577112cc4241cddf6484aaadedbf1bbd0f2351adf2e41c2fb2ecd09818080806107c0516107c0510961070051097f5f3a15bab4ce4097b1edc3a25002694b92395ce355a8a12fe557459d9633f70109818080806107805161078051096107a051097f275a20361ea91992193920270d3e2d1f6361880ac0a439c64bef815d4469ba85098180808061074051610740510961086051097f31e823a45e567484c1544e310c0fa5cd66547a8f0dde659ac61698c30e838d2509818080806107e0516107e0510961076051097f26cc223e16f47c20e17cc6069605fa5a8af05ea4f6eb36029a641d23b818eb10098180610800517f4d0ea7f9c3fda06d9535b0fdafd8338bd47c2200b284fa71a325ff41ac358028098180610820517f5b1fc262a28cbb8bf75d9b1a6edaa74591ec24cd9a209512213cec3a3c0f1a5d09610660510808080808080808816108405181808080806106805161068051096106a051097f3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc109818080806106c0516106c051096106e051097f06ccb1c7d87f3c12a2bde4e68ac7f1e8b03481ba15d7f88f9a7f9b8310dd6d3409818080806107c0516107c0510961070051097f53fded36d490ba6b05a5d10fd99ffe5456baec6a6a8753199d5ebdc33c99790e09818080806107805161078051096107a051097f412c98232b6ab8a47aa76ee814ef7ec6261987c9802f2cfc490e007951a60ca5098180808061074051610740510961086051097f333f8046ece5579cbd6872449c57f2703dfc8864cfadc06d587ff104a0d0c1f209818080806107e0516107e0510961076051097f3509dd2fe3aac0080783557fec090fb1cb4b2b0901253c55282024331d1fe1a8098180610800517f52f789e4afc3801f7411102ee2f47cc5954a744e71cac98e75ea962a55a0a76f098180610820517f590ba402032e82eb1f660ef09796c5686345a5054ed96dae8e2d2336337887710961072051080808080808080881610840515f0908090809080908090809080908090808816103005161a3005109088161a9405161a1c0510961a1c0528161a9205161a1e0510961a1e0528161a8c05161a200510961a200528161a8605161a220510961a220528161a8005161a240510961a240528161a7a05161a260510961a260528161a7405161a280510961a280528161a6e05161a2a0510961a2a0528161a6805161a2c0510961a2c0528161a5c05161a2e0510961a2e05281035f08616d80525a600360fa1b175f80a1616a2051908160015f5b6014811061499657505f5160206153025f395f51905f5280808080888180988198616da0528103600108616dc0525a600d60f81b175f80a181808061278051816127a051809c81809c81809c818c819d9b829c9a839b61704052820961706052098061702052090909090909090909617000525a601160f81b175f80a1616a4051600161738052600190617380905f915b602a831061496a57845a600960f91b175f80a1618a0061a3005261850061a320526190a061a340526190c061a3605261912061a3805261914061a3a0526191a061a3c052618a2061a3e052618a4061a40052618a6061a42052618a8061a44052618aa061a46052618ac061a48052618ae061a4a052618b0061a4c052618b2061a4e052618b4061a50052618b6061a52052618b8061a54052618ba061a56052618bc061a58052618be061a5a052618c0061a5c052618c2061a5e052618c4061a60052618c6061a62052618c8061a64052618ca061a66052618cc061a68052618ce061a6a052618d0061a6c052618d2061a6e052618d4061a70052618d6061a72052618d8061a74052618da061a76052618dc061a78052618de061a7a052618e0061a7c052618e2061a7e052618e4061a80052618e6061a82052616d8061a84052618a00516173a061a32060015b602b811061493f57505050617ba0525a601360f81b175f80a16186e0515f5160206153025f395f51905f526189a0519181806173a051928184618700510990089381836189c05109900882806173c051958187618720510990089181866189e05109900890617bc052617be0525a600560fa1b175f80a1818080619060518180619080519281886190e051099008956191005109900892818661916051099008936191805109900890617c0052617c20525a601560f81b175f80a161852061a300526185c061a3205261884061a3405261854061a360526185e061a3805261886061a3a05261856061a3c05261860061a3e05261888061a4005261858061a4205261874061a440526188a061a460526185a061a4805261876061a4a0526188c061a4c05261862061a4e05261878061a500526188e061a5205261864061a540526187a061a5605261890061a5805261866061a5a0526187c061a5c05261892061a5e05261868061a600526187e061a6205261894061a640526186a061a6605261880061a6805261896061a6a0526186c061a6c05261882061a6e05261898061a70052618520516185c05161884051916173a061a36060015b600b81106148f657505050617c4052617c6052617c80525a600b60f91b175f80a1618e8061a30052618ea061a32052618ec061a34052618ee061a36052618f0061a38052618f2061a3a052618f4061a3c052618f6061a3e052618f8061a40052618fa061a42052618fc061a44052618fe061a4605261900061a4805261902061a4a05261904061a4c052618e8051618ea051618ec051916173a061a36060015b600581106148ad57505050617ca052617cc052617ce0525a601760f81b175f80a1616a6051616a80516182a0516170005191836170205181617040519581617060519188815f5160206153025f395f51905f52035f5160206153025f395f51905f529089085f5160206153025f395f51905f528581038a085f5160206153025f395f51905f528381038b08905f5160206153025f395f51905f529109905f5160206153025f395f51905f529109915f5160206153025f395f51905f5281810383085f5160206153025f395f51905f5286810384085f5160206153025f395f51905f5290600109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52908409895f5160206153025f395f51905f5283810388085f5160206153025f395f51905f5285810389085f5160206153025f395f51905f5290600109905f5160206153025f395f51905f5291095f5160206153025f395f51905f5290830994878d5f5160206153025f395f51905f5282810387085f5160206153025f395f51905f5288810388085f5160206153025f395f51905f5290600109905f5160206153025f395f51905f5291095f5160206153025f395f51905f5290890961310090614d86565b955f5160206153025f395f51905f5283810382085f5160206153025f395f51905f5289810383085f5160206153025f395f51905f5290600109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52908809935f5160206153025f395f51905f5282810385085f5160206153025f395f51905f528a810386085f5160206153025f395f51905f5290600109905f5160206153025f395f51905f5291095f5160206153025f395f51905f529086095f5160206153025f395f51905f528381038b085f5160206153025f395f51905f528681038c085f5160206153025f395f51905f5290600109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52908209995f5160206153025f395f51905f5286810389085f5160206153025f395f51905f528281038a08905f5160206153025f395f51905f5291095f5160206153025f395f51905f52908c099a8b945f5160206153025f395f51905f52035f5160206153025f395f51905f52908a085f5160206153025f395f51905f529109905f5160206153025f395f51905f52035f5160206153025f395f51905f529089085f5160206153025f395f51905f529082099788965f5160206153025f395f51905f52035f5160206153025f395f51905f5291085f5160206153025f395f51905f529109915f5160206153025f395f51905f52910981617ca051905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f5203935f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52916080013509905f5160206153025f395f51905f529108925f5160206153025f395f51905f52910990617cc051905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52035f5160206153025f395f51905f529108925f5160206153025f395f51905f52910990617ce051905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52035f5160206153025f395f51905f5291085f5160206153025f395f51905f52825f09905f5160206153025f395f51905f52910887895f5160206153025f395f51905f5285810388085f5160206153025f395f51905f5282810389085f5160206153025f395f51905f5290600109905f5160206153025f395f51905f5291095f5160206153025f395f51905f528881038b085f5160206153025f395f51905f528781038c085f5160206153025f395f51905f528481038d08905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291098a5f5160206153025f395f51905f528a810385085f5160206153025f395f51905f5289810386085f5160206153025f395f51905f5290600109905f5160206153025f395f51905f5291095f5160206153025f395f51905f5290830991885f5160206153025f395f51905f528c810382085f5160206153025f395f51905f5287810383085f5160206153025f395f51905f5290600109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52908509968c5f5160206153025f395f51905f52878a096135c790614d86565b965f5160206153025f395f51905f52908809935f5160206153025f395f51905f5282810385085f5160206153025f395f51905f528a810386085f5160206153025f395f51905f5290600109905f5160206153025f395f51905f5291095f5160206153025f395f51905f529086095f5160206153025f395f51905f528381038b085f5160206153025f395f51905f528681038c085f5160206153025f395f51905f5290600109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52908209995f5160206153025f395f51905f5286810389085f5160206153025f395f51905f528281038a08905f5160206153025f395f51905f5291095f5160206153025f395f51905f52908c099a8b945f5160206153025f395f51905f52035f5160206153025f395f51905f52908a085f5160206153025f395f51905f529109905f5160206153025f395f51905f52035f5160206153025f395f51905f529089085f5160206153025f395f51905f529082099788965f5160206153025f395f51905f52035f5160206153025f395f51905f5291085f5160206153025f395f51905f529109915f5160206153025f395f51905f52910981617c4051905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f5203935f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52916060013509905f5160206153025f395f51905f529108925f5160206153025f395f51905f52910990617c6051905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52035f5160206153025f395f51905f529108925f5160206153025f395f51905f52910990617c8051905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52035f5160206153025f395f51905f529108915f5160206153025f395f51905f529109905f5160206153025f395f51905f529108905f5160206153025f395f51905f5281810389085f5160206153025f395f51905f52906001095f5160206153025f395f51905f5282810388085f5160206153025f395f51905f528a81038908905f5160206153025f395f51905f529109905f5160206153025f395f51905f529109905f5160206153025f395f51905f5289810382085f5160206153025f395f51905f52906001095f5160206153025f395f51905f5290830961397a90614d86565b5f5160206153025f395f51905f528a810383085f5160206153025f395f51905f52906001095f5160206153025f395f51905f52908209915f5160206153025f395f51905f528181038c085f5160206153025f395f51905f52906001095f5160206153025f395f51905f52908409905f5160206153025f395f51905f528c81038b085f5160206153025f395f51905f529083099384925f5160206153025f395f51905f528381038d085f5160206153025f395f51905f529109915f5160206153025f395f51905f52035f5160206153025f395f51905f52908c085f5160206153025f395f51905f528e81038d08905f5160206153025f395f51905f5291095f5160206153025f395f51905f52910981617c0051905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f5203915f5160206153025f395f51905f5291095f5160206153025f395f51905f529060408c013509905f5160206153025f395f51905f529108925f5160206153025f395f51905f52910990617c2051905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52035f5160206153025f395f51905f529108915f5160206153025f395f51905f529109905f5160206153025f395f51905f529108905f5160206153025f395f51905f5281810387085f5160206153025f395f51905f52906001095f5160206153025f395f51905f5282810386085f5160206153025f395f51905f528881038708905f5160206153025f395f51905f5291095f5160206153025f395f51905f52828209925f5160206153025f395f51905f5289810382085f5160206153025f395f51905f52906001095f5160206153025f395f51905f52908509613c1b90614d86565b915f5160206153025f395f51905f528a810383085f5160206153025f395f51905f52906001095f5160206153025f395f51905f52908409935f5160206153025f395f51905f52908509935f5160206153025f395f51905f528b81038a085f5160206153025f395f51905f529086099485935f5160206153025f395f51905f52035f5160206153025f395f51905f52908b085f5160206153025f395f51905f529109915f5160206153025f395f51905f52910981617bc051905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f5203915f5160206153025f395f51905f5291095f5160206153025f395f51905f529060208a013509905f5160206153025f395f51905f529108925f5160206153025f395f51905f52910990617be051905f5160206153025f395f51905f529109905f5160206153025f395f51905f5291095f5160206153025f395f51905f52035f5160206153025f395f51905f529108915f5160206153025f395f51905f529109905f5160206153025f395f51905f529108925f5160206153025f395f51905f52035f5160206153025f395f51905f529108613ddc90614d86565b90617ba0515f5160206153025f395f51905f52039035905f5160206153025f395f51905f529108905f5160206153025f395f51905f529109915f5160206153025f395f51905f529109905f5160206153025f395f51905f5291089081616e40525a600360fb1b175f80a1616aa05180808094616da051616dc051916182a051925f5160206153025f395f51905f52856001096001600160801b038116955f5160206153025f395f51905f5291096001600160801b038116965f5160206153025f395f51905f5291096001600160801b038116995f5160206153025f395f51905f5291096001600160801b038116975f5160206153025f395f51905f5291096001600160801b03169260806198c061a3005e600161a38052608061994061a3a05e6173c05161a420526080619d4061a4405e6173e05161a4c05260806199c061a4e05e6174005161a560526080619dc061a5805e6174205161a600526080619f4061a6205e6174405161a6a052608061578061a6c05e6174605161a74052608061550061a7605e6174805161a7e052608061558061a8005e6174a05161a88052608061560061a8a05e6174c05161a92052608061568061a9405e6174e05161a9c052608061570061a9e05e6175005161aa6052608061530061aa805e6175205161ab0052608061538061ab205e6175405161aba052608061540061abc05e6175605161ac4052608061548061ac605e6175805161ace052608061580061ad005e6175a05161ad8052608061588061ada05e6175c05161ae2052608061590061ae405e6175e05161aec052608061598061aee05e6176005161af60526080615b8061af805e6176205161b000526080615f0061b0205e6176405161b0a052608061600061b0c05e6176605161b14052608061608061b1605e6176805161b1e052608061610061b2005e6176a05161b28052608061618061b2a05e6176c05161b32052608061620061b3405e6176e05161b3c052608061628061b3e05e6177005161b46052608061630061b4805e6177205161b50052608061638061b5205e6177405161b5a052608061640061b5c05e6177605161b64052608061648061b6605e6177805161b6e052608061650061b7005e6177a05161b78052608061658061b7a05e6177c05161b82052608061660061b8405e6177e05161b8c052608061668061b8e05e6178005161b96052608061670061b9805e6178205161ba0052608061678061ba205e6178405161baa052608061680061bac05e6178605161bb4052608061688061bb605e6178805161bbe052608061690061bc005e6178a05161bc80526178c051915f5160206153025f395f51905f529083096080619fc061bca05e818161bd20525f5160206153025f395f51905f529109608061a04061bd405e818161bdc0525f5160206153025f395f51905f52910990608061a0c061bde05e8161be6052608061a14061be805e5f5160206153025f395f51905f52910961bf00526080615a0061bf205e61a1c0515f5160206153025f395f51905f5290820961bfa0526080615a8061bfc05e61a1e0515f5160206153025f395f51905f5290820961c040526080615b0061c0605e61a200515f5160206153025f395f51905f5290820961c0e0526080615c0061c1005e61a220515f5160206153025f395f51905f5290820961c180526080615c8061c1a05e61a240515f5160206153025f395f51905f5290820961c220526080615d0061c2405e61a260515f5160206153025f395f51905f5290820961c2c0526080615d8061c2e05e61a280515f5160206153025f395f51905f5290820961c360526080615e0061c3805e61a2a0515f5160206153025f395f51905f5290820961c400526080615e8061c4205e61a2c0515f5160206153025f395f51905f5290820961c4a0526080615f8061c4c05e61a2e0515f5160206153025f395f51905f52910961c54052608061974061c5605e8361c5e05260806197c061c6005e836173a051905f5160206153025f395f51905f52910961c68052608061984061c6a05e836173c051905f5160206153025f395f51905f52910961c720526080619cc061c7405e8461c7c0526080619e4061c7e05e846173a051905f5160206153025f395f51905f52910961c860526080619ec061c8805e846173c051905f5160206153025f395f51905f52910961c9005260806191c061c9205e8761c9a052608061924061c9c05e876173a051905f5160206153025f395f51905f52910961ca405260806192c061ca605e876173c051905f5160206153025f395f51905f52910961cae052608061934061cb005e876173e051905f5160206153025f395f51905f52910961cb805260806193c061cba05e8761740051905f5160206153025f395f51905f52910961cc2052608061944061cc405e8761742051905f5160206153025f395f51905f52910961ccc05260806194c061cce05e8761744051905f5160206153025f395f51905f52910961cd6052608061954061cd805e8761746051905f5160206153025f395f51905f52910961ce005260806195c061ce205e8761748051905f5160206153025f395f51905f52910961cea052608061964061cec05e876174a051905f5160206153025f395f51905f52910961cf405260806196c061cf605e876174c051905f5160206153025f395f51905f52910961cfe0526080619a4061d0005e8561d080526080619ac061d0a05e856173a051905f5160206153025f395f51905f52910961d120526080619b4061d1405e856173c051905f5160206153025f395f51905f52910961d1c0526080619bc061d1e05e856173e051905f5160206153025f395f51905f52910961d260526080619c4061d2805e8561740051905f5160206153025f395f51905f52910961d300526080616ac061d3205e868261d3a052614886575b5f5160206153025f395f51905f5296979387809693948180808080809a8199099c60808601350999606085013509966040840135099360208301350990350808080808616e60525a601960f81b175f80a16080616b40616f005e6080612860815e805f5160206153025f395f51905f52616e605181035f0861010052614871575b806080616e806101005e61485b575b6080616b406101005e80616a805161018052614844575b8061482e575b608080616f805e5a600760f91b175f80a17c70616972696e672d62617463682d6163632d6b7a670000000000000000610100526080616f806101205e6080616f006101a05e6080616c406102205e6080616bc06102a05e5f5160206153025f395f51905f526102206101002006908115614825575b6080616c406101005e80826101805261480e575b806080616f806101805e6147f6575b80916080616bc06101005e610180526147df575b806080616f006101805e6147c7575b5a600f60f81b175f80a18015610074576147b19061504a565b505a600160fc1b175f80a1600160805260206080f35b506080616f0061010080600b5afa60803d1416614798565b50608061010060a081600c5afa60803d1416614789565b506080616f8061010080600b5afa60803d1416614775565b50608061010060a081600c5afa60803d1416614766565b60019150614752565b5060808061010081600b5afa60803d14166146dd565b50608061010060a081600c5afa60803d14166146d7565b5060808061010081600b5afa60803d14166146c0565b5060808060a081600c5afa60803d14166146b1565b9692955090925a616e806130c061a300600c608094fa3d6080141695929693919093614630565b909194606060205f5160206153025f395f51905f526001938180808c9b9a9b519b818d8c515109900899818c878c01515109900899604089015151099008970193019101612f37565b909194606060205f5160206153025f395f51905f526001938180808c9b9a9b519b818d8c515109900899818c878c01515109900899604089015151099008970193019101612e97565b9091926020805f5160206153025f395f51905f52600193818851885151099008950193019101612cdf565b5f5160206153025f395f51905f52826020600193019509926001600160801b0384168552019192612b92565b91905f5160206153025f395f51905f528086818460019509099280099201612b01565b5f5160206153025f395f51905f5260019161030051900991828160051b61a340015201906106df565b5f61a1c08201526020016106d1565b909360205f5160206153025f395f51905f528192818835865109900895019101610681565b5f5160206153025f395f51905f5260209184519008920191610671565b5f5160206153025f395f51905f5283828280602095875109880985520991019061065c565b602091815f5160206153025f395f51905f528093810387088552099101908590610604565b915f5160206153025f395f51905f52816001920992016105ed565b9181355f5160206153025f395f51905f5281101561007457815260209081019291019061059a565b90928235915f5160206153025f395f51905f528310156100745782815291815260209081019392810192910161050c565b91906080614b00838293614e8f565b938184823701910191906104cd565b91906080614b1e838293614e8f565b9381848237019101919061048d565b9190926080614b3d838293614e8f565b9381848237019101929092919261043a565b916080614b60858293969496614e8f565b948184823701910191929192610416565b916080614b82858293969496614e8f565b9481848237019101919291926103fb565b91906080614ba2838293614e8f565b938184823701910191906103a2565b614bbf608092918392614e8f565b92818582370192019190610360565b90602080918335945f5160206153025f395f51905f5286101694815201910161033e565b506080616c4060a061a1c0600c5afa60803d1416610308565b9050614c21600160381b600760386120c4615129565b9392614c37600160381b60076038612104615272565b5093919092169580614c7b575b15614c53575b505050506102f3565b909192948383178287171715151694616c4052616c6052616c8052616ca05283808080614c4a565b95838317828617171516955f616c40525f616c60525f616c80525f616ca052614c44565b915082915f616c40525f616c60525f616c80525f616ca0526102ed565b506080616bc060a061a1c0600c5afa60803d141661026a565b9050614ceb600160381b60076038612044615129565b9392614d01600160381b60076038612084615272565b5093919092169580614d45575b15614d1d575b50505050610255565b909192948383178287171715151694616bc052616be052616c0052616c205283808080614d14565b95838317828617171516955f616bc0525f616be0525f616c00525f616c2052614d0e565b915082915f616bc0525f616be0525f616c00525f616c205261024f565b801561007457602061260052602061262052602061264052612660527f73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffffff612680525f5160206153025f395f51905f526126a052602061260060c08160055afa156100745760203d03610074576126005190565b80356040820135908060801c610074578160801c610074576001600160801b038091169116906f1a0111ea397fe69a4b1ba7b6434bacd75f5160206153425f395f51905f52602085013511158183141691101715610074576f1a0111ea397fe69a4b1ba7b6434bacd75f5160206153425f395f51905f526060840135111581831416911017156100745760809060a03761012090565b81356040830135908060801c610074578160801c610074576001600160801b038091169116906f1a0111ea397fe69a4b1ba7b6434bacd75f5160206153425f395f51905f52602086013511158183141691101715610074576f1a0111ea397fe69a4b1ba7b6434bacd75f5160206153425f395f51905f52606085013511158183141691101715610074576080809282370190565b8015615047575061a1c090616cc05191616ce0925b61706084106150225783515f5160206153025f395f51905f529109801561501b57602082526020808301526020604083015260608201527f73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffffff60808201525f5160206153025f395f51905f5260a082015260208160c08160055afa60203d141692815191601f1901905b80616ce010614ff35750505f5160206153025f395f51905f5280616ce051830991616cc051900990616cc052616ce052565b5f5160206153025f395f51905f5280835185099382519009928152601f199182019101614fc1565b505f925050565b60205f5160206153025f395f51905f5281928694965190099283865201930190614f38565b90565b801561504757506080616f806103005e6101006128e06103805e6080616f006104805e6101006129e06105005e60206103008080600f5afa60203d141661030051161561007457600190565b5f96945f969293945f1901925f5b8681106150b45750505050505050565b8483820483808260051b880135921516615121575b5087858406021c16868202610100811080615100575b156150ef575b50506001016150a4565b60ff19011b9099019860015f6150e5565b9a82821b019a61010089830111156150df579b8282610100031c019b6150df565b9003836150c9565b600193925f926003820160021c845b81811061522c57505084833510156001166151e5575b93600491849582615160960294615096565b8192919381936f1a0111ea397fe69a4b1ba7b6434bacd781145f5160206153425f395f51905f5284148116806151da575b156151ca575b6f1a0111ea397fe69a4b1ba7b6434bacd7905f5160206153425f395f51905f52600160801b891095111516911017161693565b6001860195861090960195615197565b5f9750879650615191565b936151609350815f5160206153425f395f51905f526f1a0111ea397fe69a4b1ba7b6434bacd761521b8460048181988c8b615096565b92909214911416945091509361514e565b828160021b850360049081811061526a575b50026101008110615253575b50600101615138565b9760018092991b8960051b8701351016979061524a565b90505f61523e565b9290915f926001946003830160021c5f5b81811061529b575050925f9260049261516095615096565b838160021b86036004908181106152d9575b500261010081106152c2575b50600101615283565b9760018092991b8960051b850135101697906152b9565b90505f6152ad56fe4e5280109d8f96b8bfb543a6b1af25fb56a9db616af85a90eedc558e3eb1ea2973eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff000000014997c5aa3a5fa07bcaf880a9054bef831effbd9cd58e46d9bb4fb88ef99de0db64774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff000000004382d0938a760120dd6cef8f3b90a0c38abae475e3d21e39365472b76d780272 +0x6108e06040526004361015610012575f80fd5b5f3560e01c80631e8e1e1314610078576342b7259714610030575f80fd5b34610074575f366003190112610074576040517f00000000000000000000000080b32f68a333df55da60bec44c498477a8311ede6001600160a01b03168152602090f35b5f80fd5b346100745760403660031901126100745760043567ffffffffffffffff8111610074573660238201121561007457806004013567ffffffffffffffff81116100745760243691830101116100745760243567ffffffffffffffff8111610074573660238201121561007457806004013567ffffffffffffffff8111610074576024369160051b8301011161007457611ec060409114911416156100745760017f00000000000000000000000080b32f68a333df55da60bec44c498477a8311ede803b61428114813f7f24430c63ec2017f12ec4004d66c66314f418dd77fe38dd4c2475c8f2b259e0091416156100745781612700614280923c6013612720511481166014612740511416816127e0511416600b61280051141660076128205114166038612840511416801561007457611e6060443514166013611ec43514163661214414168015610074575f90731a0111ea397fe69a4b1ba7b6434bacd764774b846120a435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa612084351416731a0111ea397fe69a4b1ba7b6434bacd764774b8461206435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa6120443514161680614c70575b15614bdc575b166080616bc061a1c05e808261a24052614bc3575b5f90731a0111ea397fe69a4b1ba7b6434bacd764774b8461212435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa612104351416731a0111ea397fe69a4b1ba7b6434bacd764774b846120e435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa6120c43514161680614ba6575b15614b12575b1680916080616c4061a1c05e61a24052614af9575b80156100745760806127005190525f60a0525f60c0525f60e0525f61010052601361012052610140611ee45b6121448110614ad557508115610074576064906191c0905b826107e48110614ab8575f5160206152095f395f51905f528592607f190160802080608052066169805260a090619940916101008201925b838310614a9a5784835f5160206152095f395f51905f5284607f190160802080608052066169a0525f5160206152095f395f51905f52602060802080608052066169c05260a0619a4061030083015b808410614a78575050619d40608083015b808410614a56575061010060806103f3858095614d96565b948181619e4037019201905b818310614a345750505f5160206152095f395f51905f526080610423838095614d96565b928181619ec0370191607f190160802080608052066169e05260a0610100619f409301925b838310614a165784835f5160206152095f395f51905f5284607f19016080208060805206616a005260a090619fc0916102008201925b8383106149f85784835f5160206152095f395f51905f5284607f19016080208060805206616a205260a090618500610cc08201905b8183106149c75750505f5160206152095f395f51905f528192607f19016080208060805206616a40525f5160206152095f395f51905f5260206080208060805206616a6052610120608061050683614d00565b928181616ac0370191607f190160802092836080526001600160801b035f5160206152095f395f51905f5260a0950616616a8052826182a052015b80821061499f57506080905f5160206152095f395f51905f52611ec493607f1901832080845206616aa05261057581614d00565b508181616b4037010361007457616a205180915f5b6014811061498457506127805192616cc0846127c0515b617060831061495f57505050506105da5f5160206152095f395f51905f525f5160206152695f395f51905f528408918261706052614e2a565b925f5160206152095f395f51905f52616cc092612760519009916127c0515b617060821061493a578585616ce05190616d00915b616e00831061491d575f92611ee4905b61214482106148f857505061706051616cc05190616e005193616cc052616ce052616d0052616d2052616d4052616d6052801561007457616a0051610300525f61a300525f5b61014081106148e957506001805b603182106148c05782618b4051618a40516185205190618a6051916185405161088052618a8051936185605194618aa0516108c0526185805190618ac0516185a0516108a052618ae05191886185c0519586946108805189618b0051905f5160206152095f395f51905f529109905f5160206152095f395f51905f529109955f5160206152095f395f51905f529109936108a0515f5160206152095f395f51905f52910992866108c051905f5160206152095f395f51905f529109925f5160206152095f395f51905f52910990610880515f5160206152095f395f51905f52908c09905f5160206152095f395f51905f528a8c095f5160206152095f395f51905f529108905f5160206152095f395f51905f529108905f5160206152095f395f51905f529108905f5160206152095f395f51905f529108905f5160206152095f395f51905f529108905f5160206152095f395f51905f529108905f5160206152095f395f51905f5291088684618b2051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5291085f5160206152095f395f51905f5290600109956103005161a30051905f5160206152095f395f51905f5291099661a1c051905f5160206152095f395f51905f52910861a1c0526108a0515f5160206152095f395f51905f52035f5160206152095f395f51905f52905f08915f5160206152095f395f51905f52035f5160206152095f395f51905f52905f089061088051905f5160206152095f395f51905f529108905f5160206152095f395f51905f529108905f5160206152095f395f51905f5291085f5160206152095f395f51905f529060010961a1e051905f5160206152095f395f51905f52910861a1e0525f5160206152095f395f51905f52035f5160206152095f395f51905f52905f08915f5160206152095f395f51905f529108905f5160206152095f395f51905f5291085f5160206152095f395f51905f529060010961a20051905f5160206152095f395f51905f529108906185e0515f5160206152095f395f51905f52035f5160206152095f395f51905f52905f089061088051905f5160206152095f395f51905f529108905f5160206152095f395f51905f5291085f5160206152095f395f51905f5290600109918261a9605261030051906103005190610300515f5160206152095f395f51905f529109905f5160206152095f395f51905f529109905f5160206152095f395f51905f52910961a3005261a360515f5160206152095f395f51905f529109905f5160206152095f395f51905f52910861a20052614120905f9061a960825b84906152ef861015611f0357600186515f1a9601959182600514611ed7575081600614611eb75781600814611e9c5781600d14611e725781601014611e4e5781601114611e2a5781602114611b8b5750806019146118405780601f146112ea5780601b14610b2b57600b14610aba575f80fd5b600384519401935f905f5160206152095f395f51905f526103005161a300510961a300525f5160206152095f395f51905f5285611fe08360f31c1661a1c0019283519061ffff8160e81c16610b13575b50089052610a47565b90621fffe0849260e31c1661a3400151900989610b0a565b505082516002909301925f925061a96090839060f01c8015610e575780600114610d2b5780600214610c4657600314610b62575f80fd5b5f5160206152095f395f51905f528080808080618b0051816185e05181035f0890088180618520518161858051918009097f404d21073985d14e432a4ad76d3fae06ca74314b950fe7b1d7f501cd31a8b374099008818061854051816185a051918009097f0b2cc8704264c6bd81bc620e9e524d4b73e9b2317679422ff7fa1603955649f10990088180618560518161862051918009097f0fdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f099008600109808452816103005161a300510961a300528161a2e05161a3605190090861a2e052610a47565b505f5160206152095f395f51905f528080808080618ae051816185c05181035f0890088180618520518161858051918009097f1b8114c381b922fd5d6d241210e2d8a68ad5744053ba9e776118de4107b51ace099008818061854051816185a051918009097f3df32e4cc4cb2ed20e5d21899cf5331775990ccaec4c09b4e3717213fcc0d7630990088180618560518161862051918009097f3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc1099008600109808452816103005161a300510961a300528161a2e05161a3605190090861a2e052610a47565b505f5160206152095f395f51905f528080807f73eda753299d7d483339d80809a1d7f7b67900f7fe6bfad98998021b7bb5732b81807f73eda753299d7d483339d80809a1d80553bca402fffe5bfeffffffff0000000181808080600160701b61852051088180600160701b6185405108600160381b0990088180600160701b6185605108600160701b099008818080618660518161868051600160381b0990086186a0518290600160701b09900881035f08900808818070241eabfffeb153ffffb9feffffffffaaab6187c0510981035f089008088180600160861b817f73eda753299d7d483339d80809a1d80553bda402fffe5b6e855000003ab000026187e051080981035f089008600109808452816103005161a300510961a3005261a240510861a24052610a47565b5061852051610400526185c051610520526185e051610480526186005161044052618540516104a0526187a0516104c0525f5160206152095f395f51905f528080807f73eda753299d7d483339d80809a1d7d340dd972492de594de627fffefcb803498180618560516187805161054052618580516105805261876051610460526185a0516104205261874051610500526186205161056052618640516104e05281808080618660518161868051600160381b0990086186a0518290600160701b09900881035f089181808061044051600160701b09818061048051600160381b096105205108089181808083600160701b0981806104a051600160381b09610400510808918180806104c0516104e05109700c8557e86f90d0d89eed6eb5349a0f88200991818080610540516104e05109702cb9b546d20373eaf85e8f53db883cb5480991818080610460516104e0510970013af65741744bd7bb2c6872df2b8003200991818080610500516104e0510970340f2ebe380a0f5eff4360543988a61dc20991818080610440516104e0510970297784894e27525bc342b7fde37dba93660991818080610480516104e05109703212e00cde6d2002b119d800000347fcb809918180806104c0516105605109702cb9b546d20373eaf85e8f53db883cb548099181808061054051610560510970013af65741744bd7bb2c6872df2b800320099181808061046051610560510970340f2ebe380a0f5eff4360543988a61dc2099181808061050051610560510970297784894e27525bc342b7fde37dba93660991818080610440516105605109703212e00cde6d2002b119d800000347fcb809918180806104c051610420510970013af65741744bd7bb2c6872df2b800320099181808061054051610420510970340f2ebe380a0f5eff4360543988a61dc2099181808061046051610420510970297784894e27525bc342b7fde37dba93660991818080610500516104205109703212e00cde6d2002b119d800000347fcb809918180806104c051610580510970340f2ebe380a0f5eff4360543988a61dc2099181808061054051610580510970297784894e27525bc342b7fde37dba93660991818080610460516105805109703212e00cde6d2002b119d800000347fcb809918180806104c051840970297784894e27525bc342b7fde37dba936609918180808080610540518609703212e00cde6d2002b119d800000347fcb80993610520519009600160701b098180806104c0516104a05109703212e00cde6d2002b119d800000347fcb809818080610480516104a05109600160701b09818080610520516104a05109600160381b09818080610440516104005109600160701b09818080610480516104005109600160381b09816105205161040051090808080808080808080808080808080808080808080808080808080808818070241eabfffeb153ffffb9feffffffffaaab6187c0510981035f089008088180600160861b817f73eda753299d7d483339d80809a1d80553b9202d7ffe85d4800008bb200000016187e051080981035f089008600109808452816103005161a300510961a3005261a220510861a22052610a47565b505090505f905f61a96090616d40516103a052616d005161032052616d20516103e0526169a05161038052616980516103c0525f5160206152095f395f51905f52806190e05181610320516103a0510809816103005161a30051090861a300525f5160206152095f395f51905f52618b6051816103c05191816103c0515f0908095f5b6004811061181357505060015f5b600481106117f15750600161a9e05260015b600481106117bf5750600161aac0526003805b61178d57505f905f5b600481106117605750905f5160206152095f395f51905f52808080808096816190c05197810391880908816103005161a3005109089381808080618be051948180618b8051816103c0515f090881618ba051916103c05190090895096190e0510881036191005108816190a0519361038051900890090881806103e05161032051088103600108099161030051900908610360526191605161034052618a00516102e05261852051610260526185405161028052618560516102c052618580516102a0526185a0516102405261862051610220526186405161020052618660516101e052618680516101c0526186a0516101a0526186c051610180526186e0516101605261870051610140526187205161012052618bc051610100526191405160e0525f5160206152095f395f51905f5280618c005181035f0860010860c0525f5160206152095f395f51905f52808060e05160010961034051088103619180510860a0525f5160206152095f395f51905f5280806191205181806103805181806101005160c05109816103c05181806101205160c05109816103c05181806101405160c05109816103c05181806101605160c05109816103c05181806101805160c05109816103c05181806101a05160c05109816103c05181806101c05160c05109816103c05181806101e05160c05109816103c05181806102005160c05109816103c05181806102205160c05109816103c05181806102405160c05109816103c05181806102a05160c05109816103c05181806102c05160c05109816103c05181806102805160c05109816103c05181806102605160c05109816103c05181806102e05160c05109816103c0515f09080908090809080908090809080908090809080908090809080908090809080860a051090881806103e0516103205108810360010809816103005181805f5160206152695f395f51905f528180610380518161010051816103c0518161012051816103c0518161014051816103c0518161016051816103c0518161018051816103c051816101a051816103c051816101c051816103c051816101e051816103c0518161020051816103c0518161022051816103c0518161024051816103c051816102a051816103c051816102c051816103c0518161028051816103c0518161026051816103c051816102e051816103c0515f09080908090809080908090809080908090809080908090809080908090809080860e0510908816103005181806103405181610320516103a051080981610300516103605109080908090861a30052610a47565b915f5160206152095f395f51905f52600191818560051b8061aa6001519061a9e0015109900892016113a9565b5f5160206152095f395f51905f528160051b808601519061aa600151095f19820160051b61aa6001525f1901806113a0565b6001905f5160206152095f395f51905f525f19820160051b808701519061a9e00151098160051b61a9e001520161138d565b905f5160206152095f395f51905f526001918360051b8601519009910161137b565b8060019160051b5f5160206152095f395f51905f526103805181836185400151870808908601520161136d565b5050618a205161a9609081525f925082805b60058110611b7257506185005161aa2052616d605161aa40525f5b60098110611b595750618a005161ab80525f5b60128110611b4057505f5b60068110611b2557505f5b60068110611b0a57505f5b60058110611aef57505f5160206152095f395f51905f5280808061ade0518103600108616d405109816103005161a30051090881808061ae80518181810391800908616d005109916103005190090861a3005260015b60068110611aa957505f5160206152095f395f51905f52616a20516169a0510961b000525f5b6006811061192b5750610a47565b600381026003810160128111611aa1575b8260051b908161aea001519161ade001519061b00051908194906169c051906169a0515b8a8285106119d457505050505050600193925f5160206152095f395f51905f52808080957f4285088329c399ea457a8ca1d30f8957e74c7f529842a1579b4fee55b398292395820390088180616d2051616d0051088103890809816103005161a30051090861a300520961b000520161191d565b9285979385969792939482809760051b809301519261aba001515f5160206152095f395f51905f529087095f5160206152095f395f51905f52908408905f5160206152095f395f51905f5291085f5160206152095f395f51905f529109985f5160206152095f395f51905f529108905f5160206152095f395f51905f5291085f5160206152095f395f51905f529109947f08634d0aa021aaf843cab354fabb0062f6502437c6a09c006c083479590189d75f5160206152095f395f51905f52910993600101929190611960565b50601261193c565b805f5160206152095f395f51905f52808060019460051b61ade001515f19850160051b61af60015182039008616d405109816103005161a30051090861a30052016118f7565b80606060019202618ec001518160051b61af600152016118a1565b80606060019202618ea001518160051b61aea0015201611896565b80606060019202618e8001518160051b61ade001520161188b565b8060019160051b80618c4001519061aba0015201611880565b8060019160051b8061862001519061aa6001520161186d565b8060019160051b8061852001519061a980015201611852565b92939480915090600181515f1a91015f9260018316611e1a575b505f9360028316611e01575b815196875f1a8860011a908960021a9a60058b60041a960199611df3575b505f5b818110611da65750505f5b818110611d475750505f5b878a821015611c7f5788519860118a60f01c9101995f5b60078110611c135750505050600101611be8565b8060051b8301515f6080525b600760805110611c325750600101611bff565b9a5f5160206152095f395f51905f5290818d81600460805187018a0101515f1a60051b612ae001519160805160051b61ffff8960e01c16015190090990089a600160805101608052611c1f565b5050959750955f905b8060031a8210611d0d5750505f905b808210611cc9575050600116611cb1575b50916001610a47565b905f5160206152095f395f51905f5291510984611ca8565b90935f5160206152095f395f51905f526001918160058b519b019a81815f1a60051b612ae001519161ffff808260d81c16519160e81c165109099008940190611c97565b90945f5160206152095f395f51905f526001918160038c519c019b61ffff8160e81c1651905f1a60051b612ae00151099008950190611c88565b6002895160f01c990198515f905b8a60078310611d6957505050600101611bdd565b600191929a5f5160206152095f395f51905f5260038193519e019d8161ffff825f1a60051b612ae001519260e81c16518709099008990190611d55565b5f5b8a60078210611dbb575050600101611bd2565b6001919a5f5160206152095f395f51905f5260038193519e019d61ffff8160e81c1651905f1a60051b612ae001510990089901611da8565b84526020909301928b611bcf565b9350600181515f1a91019060051b612ae0015193611bb1565b905160f01c925060030187611ba5565b935f5160206152095f395f51905f5291506002865160f01c96019551900992610a47565b935f5160206152095f395f51905f5291506002865160f01c96019551900892610a47565b935f5160206152095f395f51905f529150600186515f1a96019560051b612ae00151900992610a47565b935f5160206152095f395f51905f52809250035f0892610a47565b93915f5160206152095f395f51905f529150601f19019182510892610a47565b92949150946003905160f01c920194611ef5575b5051916001610a47565b835260209092019184611eeb565b83906152ef870361007457610074576169e051610840525f5160206152095f395f51905f52618ae051815f5160206152695f395f51905f526185c051099008610720526185205161082052618540516108005261856051610760525f5160206152095f395f51905f526107605161076051096107e05261858051610860525f5160206152095f395f51905f52610860516108605109610740526185a0516107a0525f5160206152095f395f51905f526107a0516107a051096107805261862051610700525f5160206152095f395f51905f526107005161070051096107c052618640516106e0525f5160206152095f395f51905f526106e0516106e051096106c052618660516106a0525f5160206152095f395f51905f526106a0516106a05109610680525f5160206152095f395f51905f52618b0051815f5160206152695f395f51905f526185e05109900861066052618b205161064052618b405161062052618a405161060052618a60516105e052618a80516105c0525f5160206152095f395f51905f52618aa051815f5160206152695f395f51905f52618600510990086105a0525f5160206152095f395f51905f5280808080618c205181036001086191a0519009810381808080806106805161068051096106a051095f5160206152295f395f51905f5209818080806106c0516106c051096106e051095f5160206152895f395f51905f5209818080806107c0516107c0510961070051095f5160206151e95f395f51905f5209818080806107805161078051096107a051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e098180808061074051610740510961086051097ed94c46a8456352aa44d7a885ab59e3a36664e6fb25e826f8a4cd79822f053309818080806107e0516107e0510961076051097f1f61345b652161410c5e29f51e301ae56342af824bc110649393d2b911c50d3e098180610800517f40fa389feb2522bb934881ac9ed749aee2296502af592418c6b5675c0f560261098180610820517f70d8f2a733a64d650faccc9b1c2a766a9544bb3ff1a11ee73cb43947ef386633096105a0510808080808080808816108405181808080806106c0516106c051096106e051095f5160206152295f395f51905f5209818080806107c0516107c0510961070051095f5160206152895f395f51905f5209818080806107805161078051096107a051095f5160206151e95f395f51905f52098180808061074051610740510961086051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e09818080806107e0516107e0510961076051097ed94c46a8456352aa44d7a885ab59e3a36664e6fb25e826f8a4cd79822f0533098180610800517f27e7119226c42a6d19c1541904b99ae40685511ed2e078964b74594d38340849098180610820517f6d05a41959f539a7fc9ec0972ea1e3dbb6fc67dd51daf3414f7fbbb091c7274a0981805f5160206152695f395f51905f526106a051096105c0510808080808080808816108405181808080806107c0516107c0510961070051095f5160206152295f395f51905f5209818080806107805161078051096107a051095f5160206152895f395f51905f52098180808061074051610740510961086051095f5160206151e95f395f51905f5209818080806107e0516107e0510961076051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e098180610800517f23a6684b942d726a22e4d5b8d8ff83aeaa773f62600184efe5d033d7c7c6e827098180610820517f2f5908b169c6cf1bd26dcf0f9e5105481f5164f3ece0582bf3098312167751a70981805f5160206152695f395f51905f526106e051096105e05108080808080808816108405181808080806107805161078051096107a051095f5160206152295f395f51905f52098180808061074051610740510961086051095f5160206152895f395f51905f5209818080806107e0516107e0510961076051095f5160206151e95f395f51905f52098180610800517f24822e1af9aa2887c912c87eb0f20bd332330e7e55cd784de67cb407a9f05520098180610820517f726df1506749848155630b86ae25a82b281ecd050fe3a52d85a181fa87202e4b0981805f5160206152695f395f51905f526107005109610600510808080808088161084051818080808061074051610740510961086051095f5160206152295f395f51905f5209818080806107e0516107e0510961076051095f5160206152895f395f51905f52098180610800517f26c2cc87f95726b28f33ca03409a460ec987cfe12adae32769e3565865d07191098180610820517f222e83e70453dfee19b402e9fa8dfe2c4987b034d0be3ceb478b3022e97934c10981805f5160206152695f395f51905f526107a05109610620510808080808816108405181808080806107e0516107e0510961076051095f5160206152295f395f51905f52098180610800517f6bd72f9cfc53af9d931896e77ea5c61244cb6d5fae8954f37dc7b9002f5aa78a098180610820517f5e1d3dbecda6214343e24a47f45c5d033197ad01b65a730af95dc57e90c491400981805f5160206152695f395f51905f5261086051096106405108080808816108405181808080806106805161068051096106a051097f0fdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f09818080806106c0516106c051096106e051097f301cf56f9b4577112cc4241cddf6484aaadedbf1bbd0f2351adf2e41c2fb2ecd09818080806107c0516107c0510961070051097f5f3a15bab4ce4097b1edc3a25002694b92395ce355a8a12fe557459d9633f70109818080806107805161078051096107a051097f275a20361ea91992193920270d3e2d1f6361880ac0a439c64bef815d4469ba85098180808061074051610740510961086051097f31e823a45e567484c1544e310c0fa5cd66547a8f0dde659ac61698c30e838d2509818080806107e0516107e0510961076051097f26cc223e16f47c20e17cc6069605fa5a8af05ea4f6eb36029a641d23b818eb10098180610800517f4d0ea7f9c3fda06d9535b0fdafd8338bd47c2200b284fa71a325ff41ac358028098180610820517f5b1fc262a28cbb8bf75d9b1a6edaa74591ec24cd9a209512213cec3a3c0f1a5d09610660510808080808080808816108405181808080806106805161068051096106a051097f3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc109818080806106c0516106c051096106e051097f06ccb1c7d87f3c12a2bde4e68ac7f1e8b03481ba15d7f88f9a7f9b8310dd6d3409818080806107c0516107c0510961070051097f53fded36d490ba6b05a5d10fd99ffe5456baec6a6a8753199d5ebdc33c99790e09818080806107805161078051096107a051097f412c98232b6ab8a47aa76ee814ef7ec6261987c9802f2cfc490e007951a60ca5098180808061074051610740510961086051097f333f8046ece5579cbd6872449c57f2703dfc8864cfadc06d587ff104a0d0c1f209818080806107e0516107e0510961076051097f3509dd2fe3aac0080783557fec090fb1cb4b2b0901253c55282024331d1fe1a8098180610800517f52f789e4afc3801f7411102ee2f47cc5954a744e71cac98e75ea962a55a0a76f098180610820517f590ba402032e82eb1f660ef09796c5686345a5054ed96dae8e2d2336337887710961072051080808080808080881610840515f0908090809080908090809080908090808816103005161a3005109088161a9405161a1c0510961a1c0528161a9205161a1e0510961a1e0528161a8c05161a200510961a200528161a8605161a220510961a220528161a8005161a240510961a240528161a7a05161a260510961a260528161a7405161a280510961a280528161a6e05161a2a0510961a2a0528161a6805161a2c0510961a2c0528161a5c05161a2e0510961a2e05281035f08616d8052616a2051908160015f5b6014811061489d57505f5160206152095f395f51905f5280808080888180988198616da0528103600108616dc05281808061278051816127a051809c81809c81809c818c819d9b829c9a839b6170405282096170605209806170205209090909090909090961700052616a4051600161738052600190617380905f915b602a83106148715784618a0061a3005261850061a320526190a061a340526190c061a3605261912061a3805261914061a3a0526191a061a3c052618a2061a3e052618a4061a40052618a6061a42052618a8061a44052618aa061a46052618ac061a48052618ae061a4a052618b0061a4c052618b2061a4e052618b4061a50052618b6061a52052618b8061a54052618ba061a56052618bc061a58052618be061a5a052618c0061a5c052618c2061a5e052618c4061a60052618c6061a62052618c8061a64052618ca061a66052618cc061a68052618ce061a6a052618d0061a6c052618d2061a6e052618d4061a70052618d6061a72052618d8061a74052618da061a76052618dc061a78052618de061a7a052618e0061a7c052618e2061a7e052618e4061a80052618e6061a82052616d8061a84052618a00516173a061a32060015b602b811061484657505050617ba0526186e0515f5160206152095f395f51905f526189a0519181806173a051928184618700510990089381836189c05109900882806173c051958187618720510990089181866189e05109900890617bc052617be052818080619060518180619080519281886190e051099008956191005109900892818661916051099008936191805109900890617c0052617c205261852061a300526185c061a3205261884061a3405261854061a360526185e061a3805261886061a3a05261856061a3c05261860061a3e05261888061a4005261858061a4205261874061a440526188a061a460526185a061a4805261876061a4a0526188c061a4c05261862061a4e05261878061a500526188e061a5205261864061a540526187a061a5605261890061a5805261866061a5a0526187c061a5c05261892061a5e05261868061a600526187e061a6205261894061a640526186a061a6605261880061a6805261896061a6a0526186c061a6c05261882061a6e05261898061a70052618520516185c05161884051916173a061a36060015b600b81106147fd57505050617c4052617c6052617c8052618e8061a30052618ea061a32052618ec061a34052618ee061a36052618f0061a38052618f2061a3a052618f4061a3c052618f6061a3e052618f8061a40052618fa061a42052618fc061a44052618fe061a4605261900061a4805261902061a4a05261904061a4c052618e8051618ea051618ec051916173a061a36060015b600581106147b457505050617ca052617cc052617ce052616a6051616a80516182a0516170005191836170205181617040519581617060519188815f5160206152095f395f51905f52035f5160206152095f395f51905f529089085f5160206152095f395f51905f528581038a085f5160206152095f395f51905f528381038b08905f5160206152095f395f51905f529109905f5160206152095f395f51905f529109915f5160206152095f395f51905f5281810383085f5160206152095f395f51905f5286810384085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52908409895f5160206152095f395f51905f5283810388085f5160206152095f395f51905f5285810389085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5290830994878d5f5160206152095f395f51905f5282810387085f5160206152095f395f51905f5288810388085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5290890961303990614c8d565b955f5160206152095f395f51905f5283810382085f5160206152095f395f51905f5289810383085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52908809935f5160206152095f395f51905f5282810385085f5160206152095f395f51905f528a810386085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f529086095f5160206152095f395f51905f528381038b085f5160206152095f395f51905f528681038c085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52908209995f5160206152095f395f51905f5286810389085f5160206152095f395f51905f528281038a08905f5160206152095f395f51905f5291095f5160206152095f395f51905f52908c099a8b945f5160206152095f395f51905f52035f5160206152095f395f51905f52908a085f5160206152095f395f51905f529109905f5160206152095f395f51905f52035f5160206152095f395f51905f529089085f5160206152095f395f51905f529082099788965f5160206152095f395f51905f52035f5160206152095f395f51905f5291085f5160206152095f395f51905f529109915f5160206152095f395f51905f52910981617ca051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5203935f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52916080013509905f5160206152095f395f51905f529108925f5160206152095f395f51905f52910990617cc051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52035f5160206152095f395f51905f529108925f5160206152095f395f51905f52910990617ce051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52035f5160206152095f395f51905f5291085f5160206152095f395f51905f52825f09905f5160206152095f395f51905f52910887895f5160206152095f395f51905f5285810388085f5160206152095f395f51905f5282810389085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f528881038b085f5160206152095f395f51905f528781038c085f5160206152095f395f51905f528481038d08905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291098a5f5160206152095f395f51905f528a810385085f5160206152095f395f51905f5289810386085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5290830991885f5160206152095f395f51905f528c810382085f5160206152095f395f51905f5287810383085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52908509968c5f5160206152095f395f51905f52878a0961350090614c8d565b965f5160206152095f395f51905f52908809935f5160206152095f395f51905f5282810385085f5160206152095f395f51905f528a810386085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f529086095f5160206152095f395f51905f528381038b085f5160206152095f395f51905f528681038c085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52908209995f5160206152095f395f51905f5286810389085f5160206152095f395f51905f528281038a08905f5160206152095f395f51905f5291095f5160206152095f395f51905f52908c099a8b945f5160206152095f395f51905f52035f5160206152095f395f51905f52908a085f5160206152095f395f51905f529109905f5160206152095f395f51905f52035f5160206152095f395f51905f529089085f5160206152095f395f51905f529082099788965f5160206152095f395f51905f52035f5160206152095f395f51905f5291085f5160206152095f395f51905f529109915f5160206152095f395f51905f52910981617c4051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5203935f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52916060013509905f5160206152095f395f51905f529108925f5160206152095f395f51905f52910990617c6051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52035f5160206152095f395f51905f529108925f5160206152095f395f51905f52910990617c8051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52035f5160206152095f395f51905f529108915f5160206152095f395f51905f529109905f5160206152095f395f51905f529108905f5160206152095f395f51905f5281810389085f5160206152095f395f51905f52906001095f5160206152095f395f51905f5282810388085f5160206152095f395f51905f528a81038908905f5160206152095f395f51905f529109905f5160206152095f395f51905f529109905f5160206152095f395f51905f5289810382085f5160206152095f395f51905f52906001095f5160206152095f395f51905f529083096138b390614c8d565b5f5160206152095f395f51905f528a810383085f5160206152095f395f51905f52906001095f5160206152095f395f51905f52908209915f5160206152095f395f51905f528181038c085f5160206152095f395f51905f52906001095f5160206152095f395f51905f52908409905f5160206152095f395f51905f528c81038b085f5160206152095f395f51905f529083099384925f5160206152095f395f51905f528381038d085f5160206152095f395f51905f529109915f5160206152095f395f51905f52035f5160206152095f395f51905f52908c085f5160206152095f395f51905f528e81038d08905f5160206152095f395f51905f5291095f5160206152095f395f51905f52910981617c0051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5203915f5160206152095f395f51905f5291095f5160206152095f395f51905f529060408c013509905f5160206152095f395f51905f529108925f5160206152095f395f51905f52910990617c2051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52035f5160206152095f395f51905f529108915f5160206152095f395f51905f529109905f5160206152095f395f51905f529108905f5160206152095f395f51905f5281810387085f5160206152095f395f51905f52906001095f5160206152095f395f51905f5282810386085f5160206152095f395f51905f528881038708905f5160206152095f395f51905f5291095f5160206152095f395f51905f52828209925f5160206152095f395f51905f5289810382085f5160206152095f395f51905f52906001095f5160206152095f395f51905f52908509613b5490614c8d565b915f5160206152095f395f51905f528a810383085f5160206152095f395f51905f52906001095f5160206152095f395f51905f52908409935f5160206152095f395f51905f52908509935f5160206152095f395f51905f528b81038a085f5160206152095f395f51905f529086099485935f5160206152095f395f51905f52035f5160206152095f395f51905f52908b085f5160206152095f395f51905f529109915f5160206152095f395f51905f52910981617bc051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5203915f5160206152095f395f51905f5291095f5160206152095f395f51905f529060208a013509905f5160206152095f395f51905f529108925f5160206152095f395f51905f52910990617be051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52035f5160206152095f395f51905f529108915f5160206152095f395f51905f529109905f5160206152095f395f51905f529108925f5160206152095f395f51905f52035f5160206152095f395f51905f529108613d1590614c8d565b90617ba0515f5160206152095f395f51905f52039035905f5160206152095f395f51905f529108905f5160206152095f395f51905f529109915f5160206152095f395f51905f529109905f5160206152095f395f51905f5291089081616e4052616aa05180808094616da051616dc051916182a051925f5160206152095f395f51905f52856001096001600160801b038116955f5160206152095f395f51905f5291096001600160801b038116965f5160206152095f395f51905f5291096001600160801b038116995f5160206152095f395f51905f5291096001600160801b038116975f5160206152095f395f51905f5291096001600160801b03169260806198c061a3005e600161a38052608061994061a3a05e6173c05161a420526080619d4061a4405e6173e05161a4c05260806199c061a4e05e6174005161a560526080619dc061a5805e6174205161a600526080619f4061a6205e6174405161a6a052608061578061a6c05e6174605161a74052608061550061a7605e6174805161a7e052608061558061a8005e6174a05161a88052608061560061a8a05e6174c05161a92052608061568061a9405e6174e05161a9c052608061570061a9e05e6175005161aa6052608061530061aa805e6175205161ab0052608061538061ab205e6175405161aba052608061540061abc05e6175605161ac4052608061548061ac605e6175805161ace052608061580061ad005e6175a05161ad8052608061588061ada05e6175c05161ae2052608061590061ae405e6175e05161aec052608061598061aee05e6176005161af60526080615b8061af805e6176205161b000526080615f0061b0205e6176405161b0a052608061600061b0c05e6176605161b14052608061608061b1605e6176805161b1e052608061610061b2005e6176a05161b28052608061618061b2a05e6176c05161b32052608061620061b3405e6176e05161b3c052608061628061b3e05e6177005161b46052608061630061b4805e6177205161b50052608061638061b5205e6177405161b5a052608061640061b5c05e6177605161b64052608061648061b6605e6177805161b6e052608061650061b7005e6177a05161b78052608061658061b7a05e6177c05161b82052608061660061b8405e6177e05161b8c052608061668061b8e05e6178005161b96052608061670061b9805e6178205161ba0052608061678061ba205e6178405161baa052608061680061bac05e6178605161bb4052608061688061bb605e6178805161bbe052608061690061bc005e6178a05161bc80526178c051915f5160206152095f395f51905f529083096080619fc061bca05e818161bd20525f5160206152095f395f51905f529109608061a04061bd405e818161bdc0525f5160206152095f395f51905f52910990608061a0c061bde05e8161be6052608061a14061be805e5f5160206152095f395f51905f52910961bf00526080615a0061bf205e61a1c0515f5160206152095f395f51905f5290820961bfa0526080615a8061bfc05e61a1e0515f5160206152095f395f51905f5290820961c040526080615b0061c0605e61a200515f5160206152095f395f51905f5290820961c0e0526080615c0061c1005e61a220515f5160206152095f395f51905f5290820961c180526080615c8061c1a05e61a240515f5160206152095f395f51905f5290820961c220526080615d0061c2405e61a260515f5160206152095f395f51905f5290820961c2c0526080615d8061c2e05e61a280515f5160206152095f395f51905f5290820961c360526080615e0061c3805e61a2a0515f5160206152095f395f51905f5290820961c400526080615e8061c4205e61a2c0515f5160206152095f395f51905f5290820961c4a0526080615f8061c4c05e61a2e0515f5160206152095f395f51905f52910961c54052608061974061c5605e8361c5e05260806197c061c6005e836173a051905f5160206152095f395f51905f52910961c68052608061984061c6a05e836173c051905f5160206152095f395f51905f52910961c720526080619cc061c7405e8461c7c0526080619e4061c7e05e846173a051905f5160206152095f395f51905f52910961c860526080619ec061c8805e846173c051905f5160206152095f395f51905f52910961c9005260806191c061c9205e8761c9a052608061924061c9c05e876173a051905f5160206152095f395f51905f52910961ca405260806192c061ca605e876173c051905f5160206152095f395f51905f52910961cae052608061934061cb005e876173e051905f5160206152095f395f51905f52910961cb805260806193c061cba05e8761740051905f5160206152095f395f51905f52910961cc2052608061944061cc405e8761742051905f5160206152095f395f51905f52910961ccc05260806194c061cce05e8761744051905f5160206152095f395f51905f52910961cd6052608061954061cd805e8761746051905f5160206152095f395f51905f52910961ce005260806195c061ce205e8761748051905f5160206152095f395f51905f52910961cea052608061964061cec05e876174a051905f5160206152095f395f51905f52910961cf405260806196c061cf605e876174c051905f5160206152095f395f51905f52910961cfe0526080619a4061d0005e8561d080526080619ac061d0a05e856173a051905f5160206152095f395f51905f52910961d120526080619b4061d1405e856173c051905f5160206152095f395f51905f52910961d1c0526080619bc061d1e05e856173e051905f5160206152095f395f51905f52910961d260526080619c4061d2805e8561740051905f5160206152095f395f51905f52910961d300526080616ac061d3205e868261d3a05261478d575b5f5160206152095f395f51905f5296979387809693948180808080809a8199099c60808601350999606085013509966040840135099360208301350990350808080808616e60526080616b40616f005e6080612860815e805f5160206152095f395f51905f52616e605181035f0861010052614778575b806080616e806101005e614762575b6080616b406101005e80616a80516101805261474b575b80614735575b608080616f805e7c70616972696e672d62617463682d6163632d6b7a670000000000000000610100526080616f806101205e6080616f006101a05e6080616c406102205e6080616bc06102a05e5f5160206152095f395f51905f52610220610100200690811561472c575b6080616c406101005e808261018052614715575b806080616f806101805e6146fd575b80916080616bc06101005e610180526146e6575b806080616f006101805e6146ce575b8015610074576146c290614f51565b50600160805260206080f35b506080616f0061010080600b5afa60803d14166146b3565b50608061010060a081600c5afa60803d14166146a4565b506080616f8061010080600b5afa60803d1416614690565b50608061010060a081600c5afa60803d1416614681565b6001915061466d565b5060808061010081600b5afa60803d1416614602565b50608061010060a081600c5afa60803d14166145fc565b5060808061010081600b5afa60803d14166145e5565b5060808060a081600c5afa60803d14166145d6565b9692955090925a616e806130c061a300600c608094fa3d608014169592969391909361455f565b909194606060205f5160206152095f395f51905f526001938180808c9b9a9b519b818d8c515109900899818c878c01515109900899604089015151099008970193019101612e7a565b909194606060205f5160206152095f395f51905f526001938180808c9b9a9b519b818d8c515109900899818c878c01515109900899604089015151099008970193019101612de4565b9091926020805f5160206152095f395f51905f52600193818851885151099008950193019101612c4a565b5f5160206152095f395f51905f52826020600193019509926001600160801b0384168552019192612b07565b91905f5160206152095f395f51905f528086818460019509099280099201612a8a565b5f5160206152095f395f51905f5260019161030051900991828160051b61a34001520190610672565b5f61a1c0820152602001610664565b909360205f5160206152095f395f51905f52819281883586510990089501910161061e565b5f5160206152095f395f51905f526020918451900892019161060e565b5f5160206152095f395f51905f528382828060209587510988098552099101906105f9565b602091815f5160206152095f395f51905f5280938103870885520991019085906105a1565b915f5160206152095f395f51905f528160019209920161058a565b9181355f5160206152095f395f51905f52811015610074578152602090810192910190610541565b90928235915f5160206152095f395f51905f52831015610074578281529181526020908101939281019291016104b3565b91906080614a07838293614d96565b9381848237019101919061047e565b91906080614a25838293614d96565b93818482370191019190610448565b9190926080614a44838293614d96565b938184823701910192909291926103ff565b916080614a67858293969496614d96565b9481848237019101919291926103db565b916080614a89858293969496614d96565b9481848237019101919291926103ca565b91906080614aa9838293614d96565b9381848237019101919061037b565b614ac6608092918392614d96565b92818582370192019190610343565b90602080918335945f5160206152095f395f51905f5286101694815201910161032b565b506080616c4060a061a1c0600c5afa60803d14166102ff565b9050614b28600160381b600760386120c4615030565b9392614b3e600160381b60076038612104615179565b5093919092169580614b82575b15614b5a575b505050506102ea565b909192948383178287171715151694616c4052616c6052616c8052616ca05283808080614b51565b95838317828617171516955f616c40525f616c60525f616c80525f616ca052614b4b565b915082915f616c40525f616c60525f616c80525f616ca0526102e4565b506080616bc060a061a1c0600c5afa60803d1416610261565b9050614bf2600160381b60076038612044615030565b9392614c08600160381b60076038612084615179565b5093919092169580614c4c575b15614c24575b5050505061024c565b909192948383178287171715151694616bc052616be052616c0052616c205283808080614c1b565b95838317828617171516955f616bc0525f616be0525f616c00525f616c2052614c15565b915082915f616bc0525f616be0525f616c00525f616c2052610246565b801561007457602061260052602061262052602061264052612660527f73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffffff612680525f5160206152095f395f51905f526126a052602061260060c08160055afa156100745760203d03610074576126005190565b80356040820135908060801c610074578160801c610074576001600160801b038091169116906f1a0111ea397fe69a4b1ba7b6434bacd75f5160206152495f395f51905f52602085013511158183141691101715610074576f1a0111ea397fe69a4b1ba7b6434bacd75f5160206152495f395f51905f526060840135111581831416911017156100745760809060a03761012090565b81356040830135908060801c610074578160801c610074576001600160801b038091169116906f1a0111ea397fe69a4b1ba7b6434bacd75f5160206152495f395f51905f52602086013511158183141691101715610074576f1a0111ea397fe69a4b1ba7b6434bacd75f5160206152495f395f51905f52606085013511158183141691101715610074576080809282370190565b8015614f4e575061a1c090616cc05191616ce0925b6170608410614f295783515f5160206152095f395f51905f5291098015614f2257602082526020808301526020604083015260608201527f73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffffff60808201525f5160206152095f395f51905f5260a082015260208160c08160055afa60203d141692815191601f1901905b80616ce010614efa5750505f5160206152095f395f51905f5280616ce051830991616cc051900990616cc052616ce052565b5f5160206152095f395f51905f5280835185099382519009928152601f199182019101614ec8565b505f925050565b60205f5160206152095f395f51905f5281928694965190099283865201930190614e3f565b90565b8015614f4e57506080616f806103005e6101006128e06103805e6080616f006104805e6101006129e06105005e60206103008080600f5afa60203d141661030051161561007457600190565b5f96945f969293945f1901925f5b868110614fbb5750505050505050565b8483820483808260051b880135921516615028575b5087858406021c16868202610100811080615007575b15614ff6575b5050600101614fab565b60ff19011b9099019860015f614fec565b9a82821b019a6101008983011115614fe6579b8282610100031c019b614fe6565b900383614fd0565b600193925f926003820160021c845b81811061513357505084833510156001166150ec575b93600491849582615067960294614f9d565b8192919381936f1a0111ea397fe69a4b1ba7b6434bacd781145f5160206152495f395f51905f5284148116806150e1575b156150d1575b6f1a0111ea397fe69a4b1ba7b6434bacd7905f5160206152495f395f51905f52600160801b891095111516911017161693565b600186019586109096019561509e565b5f9750879650615098565b936150679350815f5160206152495f395f51905f526f1a0111ea397fe69a4b1ba7b6434bacd76151228460048181988c8b614f9d565b929092149114169450915093615055565b828160021b8503600490818110615171575b5002610100811061515a575b5060010161503f565b9760018092991b8960051b87013510169790615151565b90505f615145565b9290915f926001946003830160021c5f5b8181106151a2575050925f9260049261506795614f9d565b838160021b86036004908181106151e0575b500261010081106151c9575b5060010161518a565b9760018092991b8960051b850135101697906151c0565b90505f6151b456fe4e5280109d8f96b8bfb543a6b1af25fb56a9db616af85a90eedc558e3eb1ea2973eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff000000014997c5aa3a5fa07bcaf880a9054bef831effbd9cd58e46d9bb4fb88ef99de0db64774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff000000004382d0938a760120dd6cef8f3b90a0c38abae475e3d21e39365472b76d780272 diff --git a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.sol b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.sol index 269e084a9..b7a25c83b 100644 --- a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.sol +++ b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.sol @@ -288,7 +288,7 @@ contract Halo2Verifier { function verifyProof( bytes calldata proof, uint256[] calldata instances - ) external returns (bool) { + ) external view returns (bool) { // Cheap ABI-shape guard before any generated memory work: // - proof head must point at the bytes payload; // - instances head must point at the generated instance array. @@ -880,20 +880,11 @@ contract Halo2Verifier { } - // Section-boundary gas-attribution checkpoint. Emits a - // single LOG1 (no data) with topic = (id << 248) | gas(). - // Cost: 375 (LOG base) + 375 (1 topic) = 750 gas/call. - // Host-side parses the topic into (id, gas_left) and prints - // pairwise deltas (see `dump_gas_checkpoints`). - function gas_checkpoint(id) { - log1(0, 0, or(shl(248, id), gas())) - } let r := FR_MODULUS let success := true - gas_checkpoint(1) // entry: before VK loading // =============================================================== // VK loading: either bake in the embedded VK bytes or fetch @@ -982,7 +973,6 @@ contract Halo2Verifier { // where the verifier converts failure to a revert. success := validate_public_accumulator(success, r) if iszero(success) { revert(0, 0) } - gas_checkpoint(2) // after VK loading + accumulator public-input precheck // =============================================================== // Transcript: VK digest + instances + proof. @@ -1053,7 +1043,6 @@ contract Halo2Verifier { } if iszero(success) { revert(0, 0) } } - gas_checkpoint(3) // after VK digest + committed_pi + instance absorbs // =============================================================== // Per-user-phase reads + challenge squeezes. @@ -1091,7 +1080,6 @@ contract Halo2Verifier { advice_walk := add(advice_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) } - gas_checkpoint(4) // after user-phase advice reads + user challenge squeezes // ---- theta ---- // From this point onward the transcript alternates between @@ -1111,7 +1099,6 @@ contract Halo2Verifier { lookup_m_walk := add(lookup_m_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) } - gas_checkpoint(5) // after theta squeeze + lookup multiplicities // ---- beta, gamma ---- // beta and gamma are the permutation/lookup randomizers. They are @@ -1131,7 +1118,6 @@ contract Halo2Verifier { perm_z_walk := add(perm_z_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) } - gas_checkpoint(6) // after beta/gamma + permutation Z products // ---- lookup helpers + accumulators (per-lookup) ---- // Each lookup contributes zero or more helper commitments followed // by its lookup accumulator Z commitment. The generated layout keeps @@ -1172,7 +1158,6 @@ contract Halo2Verifier { calldatacopy(lookup_z_walk, proof_cptr, 0x80) lookup_z_walk := add(lookup_z_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) - gas_checkpoint(7) // after lookup helpers + Z accumulators // ---- trash_challenge ---- // Midnight squeezes this challenge unconditionally, even when the @@ -1192,7 +1177,6 @@ contract Halo2Verifier { trashcan_walk := add(trashcan_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) } - gas_checkpoint(8) // after trash_challenge + trashcans // ---- y ---- // y batches all quotient identities. Quotient commitments are read @@ -1216,7 +1200,6 @@ contract Halo2Verifier { quotient_walk := add(quotient_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) } - gas_checkpoint(9) // after y squeeze + quotient-limb reads // ---- x ---- // x is the main evaluation point. Values read after this point are @@ -1325,7 +1308,6 @@ contract Halo2Verifier { // `success` carries deferred canonicality failures from public // instance reads. G1/proof scalar helpers revert immediately. if iszero(success) { revert(0, 0) } - gas_checkpoint(10) // after evaluations + x1/x2 + f_com + x3 + q_evals + x4 + pi (transcript done) // =============================================================== // Lagrange & instance-evaluation block (pure Fr arithmetic). @@ -1405,7 +1387,6 @@ contract Halo2Verifier { mstore(L_0_MPTR, l_0) mstore(INSTANCE_EVAL_MPTR, instance_eval) } - gas_checkpoint(11) // after Lagrange + instance evaluation block if iszero(success) { revert(0, 0) } @@ -2837,7 +2818,6 @@ contract Halo2Verifier { mstore(QUOTIENT_EVAL_MPTR, linearization_expected_eval) pop(y) } - gas_checkpoint(12) // after batched identity numerator reconstruction // =============================================================== // Prepare linearization scalars for the final PCS MSM. @@ -2879,7 +2859,6 @@ contract Halo2Verifier { mstore(QUOTIENT_MPTR, x_split) mstore(add(QUOTIENT_MPTR, 0x20), one_minus_x_n) } - gas_checkpoint(13) // after linearization scalar prep // =============================================================== // PCS computation (multi-prepare emitter from Step 5). @@ -2918,7 +2897,6 @@ contract Halo2Verifier { x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) mstore(add(ROT_POINTS_MPTR, 0x0), x_pow_of_omega) } - gas_checkpoint(17) // after PCS sub-block 1 // Generated PCS sub-block 2. These lines are // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. @@ -2934,7 +2912,6 @@ contract Halo2Verifier { mstore(p, and(acc, 0xffffffffffffffffffffffffffffffff)) } } - gas_checkpoint(18) // after PCS sub-block 2 // Generated PCS sub-block 3. These lines are // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. @@ -2995,7 +2972,6 @@ contract Halo2Verifier { } mstore(add(Q_EVAL_SET_MPTR, 0x0), q_eval_set_0) } - gas_checkpoint(19) // after PCS sub-block 3 // Generated PCS sub-block 4. These lines are // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. @@ -3010,7 +2986,6 @@ contract Halo2Verifier { mstore(add(Q_EVAL_SET_MPTR, 0x20), q_eval_set_0) mstore(add(Q_EVAL_SET_MPTR, 0x40), q_eval_set_1) } - gas_checkpoint(20) // after PCS sub-block 4 // Generated PCS sub-block 5. These lines are // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. @@ -3025,7 +3000,6 @@ contract Halo2Verifier { mstore(add(Q_EVAL_SET_MPTR, 0x60), q_eval_set_0) mstore(add(Q_EVAL_SET_MPTR, 0x80), q_eval_set_1) } - gas_checkpoint(21) // after PCS sub-block 5 // Generated PCS sub-block 6. These lines are // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. @@ -3082,7 +3056,6 @@ contract Halo2Verifier { mstore(add(Q_EVAL_SET_MPTR, 0xc0), q_eval_set_1) mstore(add(Q_EVAL_SET_MPTR, 0xe0), q_eval_set_2) } - gas_checkpoint(22) // after PCS sub-block 6 // Generated PCS sub-block 7. These lines are // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. @@ -3121,7 +3094,6 @@ contract Halo2Verifier { mstore(add(Q_EVAL_SET_MPTR, 0x120), q_eval_set_1) mstore(add(Q_EVAL_SET_MPTR, 0x140), q_eval_set_2) } - gas_checkpoint(23) // after PCS sub-block 7 // Generated PCS sub-block 8. These lines are // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. @@ -3290,7 +3262,6 @@ contract Halo2Verifier { } mstore(F_EVAL_MPTR, f_eval) } - gas_checkpoint(24) // after PCS sub-block 8 // Generated PCS sub-block 9. These lines are // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. @@ -3485,7 +3456,6 @@ contract Halo2Verifier { } mstore(V_MPTR, v) } - gas_checkpoint(25) // after PCS sub-block 9 // Generated PCS sub-block 10. These lines are // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. @@ -3517,7 +3487,6 @@ contract Halo2Verifier { mcopy(PAIRING_RHS_MPTR, 0x80, 0x80) } } - gas_checkpoint(14) // after PCS computation block (= sub-block 6) // Batch the prevalidated public IVC accumulator pairing equation // into the final KZG pairing. @@ -3576,7 +3545,6 @@ contract Halo2Verifier { success := and(success, eq(returndatasize(), 0x80)) } } - gas_checkpoint(15) // after public accumulator pairing batch prep (omitted for no-accumulator VKs) // The Yul `ec_pairing` helper checks // e(arg0, G2_BASE) * e(arg1, NEG_S_G2_BASE) == 1 @@ -3593,7 +3561,6 @@ contract Halo2Verifier { // pairing argument order. Pass them swapped to ec_pairing. if iszero(success) { revert(0, 0) } success := ec_pairing(success, PAIRING_RHS_MPTR, PAIRING_LHS_MPTR) - gas_checkpoint(16) // after final ec_pairing diff --git a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md index 4a2996664..0a2c41b79 100644 --- a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md +++ b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md @@ -8,31 +8,34 @@ chain. | Contract | Address | | --- | --- | -| `Halo2VerifyingKey` | [`0x0D2789bcDF4A0406Dc7383BD7D4b73A7f08CaC16`](https://sepolia.etherscan.io/address/0x0D2789bcDF4A0406Dc7383BD7D4b73A7f08CaC16) | -| `Halo2Verifier` | [`0xA826f6e66567EAFCd618175041a8F9E79f4661a2`](https://sepolia.etherscan.io/address/0xA826f6e66567EAFCd618175041a8F9E79f4661a2) | +| `Halo2VerifyingKey` | [`0x80b32f68A333dF55Da60BEc44C498477a8311eDe`](https://sepolia.etherscan.io/address/0x80b32f68A333dF55Da60BEc44C498477a8311eDe) | +| `Halo2Verifier` | [`0x5CfEd44D16F994fc17f681A83FEdFEB9c3348c17`](https://sepolia.etherscan.io/address/0x5CfEd44D16F994fc17f681A83FEdFEB9c3348c17) | ## Transactions | Action | Transaction | | --- | --- | -| Deploy VK | [`0x22db64c0b4a15460d93b3091359f7988123be59b945cec4bcb91b3498587f588`](https://sepolia.etherscan.io/tx/0x22db64c0b4a15460d93b3091359f7988123be59b945cec4bcb91b3498587f588) | -| Deploy verifier | [`0x5bb4769deb461e2ca68a8ebaf1fbf44a0dbe9c526e4d7795b394a0292a853882`](https://sepolia.etherscan.io/tx/0x5bb4769deb461e2ca68a8ebaf1fbf44a0dbe9c526e4d7795b394a0292a853882) | -| Verify fresh proof | [`0x696e5b99fbdfc31adc71046d2afd01897a20f05202a46c2f0e70614853e9ac90`](https://sepolia.etherscan.io/tx/0x696e5b99fbdfc31adc71046d2afd01897a20f05202a46c2f0e70614853e9ac90) | +| Deploy VK | [`0xf96f25bd4f44b815a93b5d3a8f3c59d8f62afd4d6a10adf60139c73c5b7243dc`](https://sepolia.etherscan.io/tx/0xf96f25bd4f44b815a93b5d3a8f3c59d8f62afd4d6a10adf60139c73c5b7243dc) | +| Deploy verifier | [`0x5b34c4f25cdd6a641150488b0dc5edb130d0e8fd160408789f8ecd09f99bf417`](https://sepolia.etherscan.io/tx/0x5b34c4f25cdd6a641150488b0dc5edb130d0e8fd160408789f8ecd09f99bf417) | +| Verify fresh proof | [`0x013fb1a6323d51574c0c5b1c2e79ec4fb84706474c1c06601fa6e61737eb15a4`](https://sepolia.etherscan.io/tx/0x013fb1a6323d51574c0c5b1c2e79ec4fb84706474c1c06601fa6e61737eb15a4) | ## Gas | Action | Gas used | | --- | ---: | | Deploy VK | `3,723,458` | -| Deploy verifier | `5,349,137` | -| Verify fresh proof | `1,310,991` | +| Deploy verifier | `5,295,315` | +| Verify fresh proof | `1,291,730` | + +The verifier was rendered without gas checkpoints. The proof verification +transaction emitted zero logs. ## Runtime Metadata | Contract | Runtime bytes | Runtime code hash | | --- | ---: | --- | | `Halo2VerifyingKey` | `17025` | `0x24430c63ec2017f12ec4004d66c66314f418dd77fe38dd4c2475c8f2b259e009` | -| `Halo2Verifier` | `21410` | `0x2bebc2f2ae0446f93f97cda1a72c5b6b58b9aead3a3eaf3b12927ae1bd92d981` | +| `Halo2Verifier` | `21161` | `0x2e1e46a83888a8989698b1f3ad87387db52408e015b11d7cff24a930b0baa776` | ## Files diff --git a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/deployment.json b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/deployment.json index 13d43c119..0015e53f4 100644 --- a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/deployment.json +++ b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/deployment.json @@ -4,8 +4,8 @@ "deployer": "0x609A4Ff8347Bb82d65a12A597298F256Fd4DE493", "contracts": { "Halo2VerifyingKey": { - "address": "0x0D2789bcDF4A0406Dc7383BD7D4b73A7f08CaC16", - "deploymentTx": "0x22db64c0b4a15460d93b3091359f7988123be59b945cec4bcb91b3498587f588", + "address": "0x80b32f68A333dF55Da60BEc44C498477a8311eDe", + "deploymentTx": "0xf96f25bd4f44b815a93b5d3a8f3c59d8f62afd4d6a10adf60139c73c5b7243dc", "gasUsed": 3723458, "runtimeBytes": 17025, "runtimeCodeHash": "0x24430c63ec2017f12ec4004d66c66314f418dd77fe38dd4c2475c8f2b259e009", @@ -13,20 +13,24 @@ "sourceFile": "Halo2VerifyingKey.sol" }, "Halo2Verifier": { - "address": "0xA826f6e66567EAFCd618175041a8F9E79f4661a2", - "deploymentTx": "0x5bb4769deb461e2ca68a8ebaf1fbf44a0dbe9c526e4d7795b394a0292a853882", - "gasUsed": 5349137, - "runtimeBytes": 21410, - "runtimeCodeHash": "0x2bebc2f2ae0446f93f97cda1a72c5b6b58b9aead3a3eaf3b12927ae1bd92d981", + "address": "0x5CfEd44D16F994fc17f681A83FEdFEB9c3348c17", + "deploymentTx": "0x5b34c4f25cdd6a641150488b0dc5edb130d0e8fd160408789f8ecd09f99bf417", + "gasUsed": 5295315, + "runtimeBytes": 21161, + "runtimeCodeHash": "0x2e1e46a83888a8989698b1f3ad87387db52408e015b11d7cff24a930b0baa776", "runtimeBytecodeFile": "Halo2Verifier.runtime.bytecode.hex", "sourceFile": "Halo2Verifier.sol" } }, "proofVerificationTx": { - "transactionHash": "0x696e5b99fbdfc31adc71046d2afd01897a20f05202a46c2f0e70614853e9ac90", - "gasUsed": 1310991, + "transactionHash": "0x013fb1a6323d51574c0c5b1c2e79ec4fb84706474c1c06601fa6e61737eb15a4", + "gasUsed": 1291730, "status": 1 }, + "diagnostics": { + "gasCheckpoints": false, + "proofVerificationLogs": 0 + }, "compiler": { "solc": "0.8.30", "viaIr": true, From 6467f3d2f84c30bf60ad5dbd5599480a03449117 Mon Sep 17 00:00:00 2001 From: Julien Coolen Date: Tue, 19 May 2026 12:12:32 +0100 Subject: [PATCH 17/19] Allow full-width outer PCS challenges --- aggregation/src/ivc/prover.rs | 7 +- aggregation/src/ivc/verifier.rs | 26 ++- proofs/solidity-verifier/Cargo.toml | 9 +- proofs/solidity-verifier/src/lib.rs | 8 + .../src/lowering/artifacts.rs | 4 +- .../src/lowering/render/models.rs | 4 +- .../tests/ivc_keccak_solidity.rs | 44 +++-- proofs/src/poly/kzg/mod.rs | 178 +++++++++++++----- 8 files changed, 206 insertions(+), 74 deletions(-) diff --git a/aggregation/src/ivc/prover.rs b/aggregation/src/ivc/prover.rs index 7b7a79e3f..bfb3f7163 100644 --- a/aggregation/src/ivc/prover.rs +++ b/aggregation/src/ivc/prover.rs @@ -244,7 +244,12 @@ impl IvcProver { // The ONLY difference from `prove_step`: emit the new proof under a // Keccak transcript. The IVC circuit's gates are unchanged - the - // outer Fiat-Shamir transcript is the only thing that switches. + // outer Fiat-Shamir transcript is the only thing that switches. The + // final Solidity-facing proof also uses full-width PCS challenges; the + // in-circuit verifier gadgets for the previous proof still use their + // compile-time truncated-challenge rules. + let _outer_challenge_layout = + midnight_proofs::poly::kzg::scoped_truncated_challenges(false); let proof = midnight_zk_stdlib::prove::, Keccak256>( &self.params, &self.pk, diff --git a/aggregation/src/ivc/verifier.rs b/aggregation/src/ivc/verifier.rs index b6eaf856f..aa244ee20 100644 --- a/aggregation/src/ivc/verifier.rs +++ b/aggregation/src/ivc/verifier.rs @@ -129,16 +129,22 @@ impl IvcVerifier { let pi = IvcCircuit::::format_instance(instance).map_err(|_| IvcError::InvalidInstance)?; - let mut transcript = CircuitTranscript::::init_from_bytes(proof); - let dual_msm = plonk::prepare::, CircuitTranscript>( - self.vk.vk(), - &[&[C::identity()]], - &[&[&pi]], - &mut transcript, - ) - .map_err(|_| IvcError::InvalidProof)?; - - transcript.assert_empty().map_err(|_| IvcError::TranscriptNotEmpty)?; + let dual_msm = { + let _outer_challenge_layout = + midnight_proofs::poly::kzg::scoped_truncated_challenges(false); + let mut transcript = CircuitTranscript::::init_from_bytes(proof); + let dual_msm = + plonk::prepare::, CircuitTranscript>( + self.vk.vk(), + &[&[C::identity()]], + &[&[&pi]], + &mut transcript, + ) + .map_err(|_| IvcError::InvalidProof)?; + + transcript.assert_empty().map_err(|_| IvcError::TranscriptNotEmpty)?; + dual_msm + }; let proof_acc = Accumulator::from_dual_msm(dual_msm, "self_vk", &fixed_bases); diff --git a/proofs/solidity-verifier/Cargo.toml b/proofs/solidity-verifier/Cargo.toml index 964184904..3aa5006c6 100644 --- a/proofs/solidity-verifier/Cargo.toml +++ b/proofs/solidity-verifier/Cargo.toml @@ -65,12 +65,17 @@ solidity-gas-checkpoints = [] # bits at the point of use; the internal full-precision # accumulator is preserved so power i+1 keeps full entropy. # Required for proofs from a midnight-proofs prover compiled with -# matching feature settings; using the wrong setting on either side -# silently produces invalid pairings. +# matching feature settings; callers can still scope the final outer proof to +# full-width challenges with `scoped_truncated_challenges(false)`. truncated-challenges = [ "midnight-proofs/truncated-challenges", "midnight-aggregation/truncated-challenges", ] +# Enables truncated PCS challenges for the final Solidity-facing proof. Off by +# default so Moonlight/IVC wrap proofs can use full-width challenges while +# recursive proofs verified inside the circuit keep the dependency-level +# `truncated-challenges` behavior. +outer-truncated-challenges = [] # Enables fewer-point-sets inside recursive verifier circuits and for the # leaf proofs they consume. The final Solidity-facing proof can still disable # proof-system dummy queries at runtime. diff --git a/proofs/solidity-verifier/src/lib.rs b/proofs/solidity-verifier/src/lib.rs index 011af0a58..0fd720743 100644 --- a/proofs/solidity-verifier/src/lib.rs +++ b/proofs/solidity-verifier/src/lib.rs @@ -49,6 +49,14 @@ pub const SOLIDITY_GAS_CHECKPOINTS_ENABLED: bool = cfg!(feature = "solidity-gas- /// extra dummy eval scalars. pub const OUTER_FEWER_POINT_SETS_ENABLED: bool = cfg!(feature = "outer-fewer-point-sets"); +/// Whether the generated Solidity verifier expects the outer proof to use +/// truncated KZG PCS challenges. +/// +/// This is intentionally separate from recursive/in-circuit verifier proofs: +/// final Solidity-facing proofs can opt out while recursive proofs verified +/// inside the decider circuit keep the `midnight-circuits` truncation rules. +pub const OUTER_TRUNCATED_CHALLENGES_ENABLED: bool = cfg!(feature = "outer-truncated-challenges"); + /// Whether the generated Solidity verifier expects the outer proof to use /// Midnight's single-H quotient commitment layout. /// diff --git a/proofs/solidity-verifier/src/lowering/artifacts.rs b/proofs/solidity-verifier/src/lowering/artifacts.rs index 4dbf611ed..94b880477 100644 --- a/proofs/solidity-verifier/src/lowering/artifacts.rs +++ b/proofs/solidity-verifier/src/lowering/artifacts.rs @@ -113,7 +113,7 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { &plan.meta, &plan.data, &plan.memory, - cfg!(feature = "truncated-challenges"), + midnight_proofs::poly::kzg::truncated_challenges_enabled(), trace, ); @@ -266,7 +266,7 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { quotient_identity_trace_base: layout::trace::QUOTIENT_IDENTITY_BASE, selector_trace_base: layout::trace::SELECTOR_FOLD_BASE, fixed_comm_mptr: fixed_comm_mptr_byte, - truncated_challenges: cfg!(feature = "truncated-challenges"), + truncated_challenges: midnight_proofs::poly::kzg::truncated_challenges_enabled(), expected_has_accumulator, expected_has_accumulator_word: expected_has_accumulator as usize, expected_acc_offset, diff --git a/proofs/solidity-verifier/src/lowering/render/models.rs b/proofs/solidity-verifier/src/lowering/render/models.rs index 0ff85f228..5717e70aa 100644 --- a/proofs/solidity-verifier/src/lowering/render/models.rs +++ b/proofs/solidity-verifier/src/lowering/render/models.rs @@ -507,8 +507,8 @@ pub(crate) struct Halo2Verifier { /// - x1 / x4 powers are masked to 128 bits at use, with the internal /// full-precision accumulator preserved /// - /// Driven by `cfg!(feature = "truncated-challenges")` in - /// `SolidityGenerator::generate_verifier`. + /// Driven by `midnight_proofs::poly::kzg::truncated_challenges_enabled()` + /// in `SolidityGenerator::generate_verifier`. pub(crate) truncated_challenges: bool, /// Accumulator metadata expected by this generated verifier. These /// constants mirror the generator-side `AccumulatorEncoding` and are diff --git a/proofs/solidity-verifier/tests/ivc_keccak_solidity.rs b/proofs/solidity-verifier/tests/ivc_keccak_solidity.rs index eea83735a..dd65cb964 100644 --- a/proofs/solidity-verifier/tests/ivc_keccak_solidity.rs +++ b/proofs/solidity-verifier/tests/ivc_keccak_solidity.rs @@ -78,7 +78,7 @@ use midnight_proofs::{ poly::{ kzg::{ params::{ParamsKZG, ParamsVerifierKZG}, - scoped_fewer_point_sets, KZGCommitmentScheme, + scoped_fewer_point_sets, scoped_truncated_challenges, KZGCommitmentScheme, }, EvaluationDomain, }, @@ -1020,6 +1020,7 @@ fn ivc_final_keccak_solidity_e2e() { start.elapsed() ); let outer_fewer_point_sets = halo2_solidity_verifier::OUTER_FEWER_POINT_SETS_ENABLED; + let outer_truncated_challenges = halo2_solidity_verifier::OUTER_TRUNCATED_CHALLENGES_ENABLED; if outer_single_h { println!( "[ivc-keccak-solidity] outer proof single-H commitment: enabled (one quotient commitment expected)" @@ -1038,10 +1039,20 @@ fn ivc_final_keccak_solidity_e2e() { "[ivc-keccak-solidity] outer proof fewer-point-sets: disabled (in-circuit verifier still uses fewer-point-sets)" ); } + if outer_truncated_challenges { + println!( + "[ivc-keccak-solidity] outer proof truncated challenges: enabled (128-bit PCS challenge powers expected)" + ); + } else { + println!( + "[ivc-keccak-solidity] outer proof truncated challenges: disabled (in-circuit verifier still uses truncated challenges)" + ); + } let t0 = Instant::now(); let final_proof = { let _outer_proof_layout = scoped_fewer_point_sets(outer_fewer_point_sets); + let _outer_challenge_layout = scoped_truncated_challenges(outer_truncated_challenges); midnight_zk_stdlib::prove::( &decider_srs, &decider_pk, @@ -1066,6 +1077,7 @@ fn ivc_final_keccak_solidity_e2e() { let t0 = Instant::now(); { let _outer_proof_layout = scoped_fewer_point_sets(outer_fewer_point_sets); + let _outer_challenge_layout = scoped_truncated_challenges(outer_truncated_challenges); midnight_zk_stdlib::verify::( &decider_srs.verifier_params(), &decider_vk, @@ -1082,6 +1094,7 @@ fn ivc_final_keccak_solidity_e2e() { #[cfg(feature = "rust-verifier-trace")] let rust_trace = { let _outer_proof_layout = scoped_fewer_point_sets(outer_fewer_point_sets); + let _outer_challenge_layout = scoped_truncated_challenges(outer_truncated_challenges); collect_native_midfall_trace( &decider_srs.verifier_params(), &decider_vk, @@ -1133,19 +1146,22 @@ fn ivc_final_keccak_solidity_e2e() { let quotient_pin_address = pin_evm.create(quotient_creation_code.clone()); let quotient_runtime_size = pin_evm.code_size(quotient_pin_address); let quotient_codehash = pin_evm.code_hash(quotient_pin_address); - let artifacts = generator - .render(RenderOptions { - vk: RenderVk::Separate, - quotient: RenderQuotient::ExternalPinned { - runtime_len: quotient_runtime_size, - codehash: quotient_codehash, - }, - diagnostics: RenderDiagnostics { - trace: quotient_trace_enabled, - gas_checkpoints: gas_checkpoints_enabled, - }, - }) - .expect("pinned quotient render should succeed"); + let artifacts = { + let _outer_challenge_layout = scoped_truncated_challenges(outer_truncated_challenges); + generator + .render(RenderOptions { + vk: RenderVk::Separate, + quotient: RenderQuotient::ExternalPinned { + runtime_len: quotient_runtime_size, + codehash: quotient_codehash, + }, + diagnostics: RenderDiagnostics { + trace: quotient_trace_enabled, + gas_checkpoints: gas_checkpoints_enabled, + }, + }) + .expect("pinned quotient render should succeed") + }; let verifier_solidity = artifacts.verifier; let vk_solidity = artifacts.verifying_key.expect("separate render includes VK"); let pinned_quotient_solidity = diff --git a/proofs/src/poly/kzg/mod.rs b/proofs/src/poly/kzg/mod.rs index b30940fd1..f3f55c50d 100644 --- a/proofs/src/poly/kzg/mod.rs +++ b/proofs/src/poly/kzg/mod.rs @@ -130,6 +130,112 @@ use crate::{ }, }; +#[cfg(feature = "truncated-challenges")] +mod truncated_challenges_runtime { + use std::cell::Cell; + + thread_local! { + static ENABLED: Cell = const { Cell::new(true) }; + } + + pub fn enabled() -> bool { + ENABLED.with(Cell::get) + } + + /// Guard that restores the previous truncated-challenges setting when + /// dropped. + #[derive(Debug)] + pub struct ScopedTruncatedChallenges { + previous: bool, + } + + pub fn scoped(enabled: bool) -> ScopedTruncatedChallenges { + let previous = ENABLED.with(|cell| { + let previous = cell.get(); + cell.set(enabled); + previous + }); + ScopedTruncatedChallenges { previous } + } + + impl Drop for ScopedTruncatedChallenges { + fn drop(&mut self) { + ENABLED.with(|cell| cell.set(self.previous)); + } + } +} + +#[cfg(feature = "truncated-challenges")] +pub use truncated_challenges_runtime::ScopedTruncatedChallenges; + +/// No-op guard returned when the proof-system truncated-challenges capability +/// is not compiled in. +#[cfg(not(feature = "truncated-challenges"))] +#[derive(Debug)] +pub struct ScopedTruncatedChallenges; + +/// Returns whether KZG PCS challenge truncation is currently enabled. +/// +/// With the `truncated-challenges` feature compiled in, the default is `true` +/// to preserve the historical feature behavior. Call +/// [`scoped_truncated_challenges`] to temporarily override it for a specific +/// outer proof. +pub fn truncated_challenges_enabled() -> bool { + #[cfg(feature = "truncated-challenges")] + { + truncated_challenges_runtime::enabled() + } + #[cfg(not(feature = "truncated-challenges"))] + { + false + } +} + +/// Temporarily enables or disables KZG PCS challenge truncation on this thread. +/// +/// This is intentionally scoped so recursive proving can keep truncated +/// challenges for proofs verified inside a circuit while a final +/// Solidity-facing proof in the same process can be emitted with full-width PCS +/// challenges. +pub fn scoped_truncated_challenges(enabled: bool) -> ScopedTruncatedChallenges { + #[cfg(feature = "truncated-challenges")] + { + truncated_challenges_runtime::scoped(enabled) + } + #[cfg(not(feature = "truncated-challenges"))] + { + let _ = enabled; + ScopedTruncatedChallenges + } +} + +fn pcs_challenge(challenge: F) -> F { + #[cfg(feature = "truncated-challenges")] + { + if truncated_challenges_enabled() { + return truncate(challenge); + } + } + challenge +} + +fn pcs_powers(base: F, len: usize) -> Vec { + #[cfg(feature = "truncated-challenges")] + { + if truncated_challenges_enabled() { + return truncated_powers(base).take(len).collect(); + } + } + + let mut acc = F::ONE; + let mut out = Vec::with_capacity(len); + for _ in 0..len { + out.push(pcs_challenge(acc)); + acc *= base; + } + out +} + #[derive(Clone, Debug)] /// KZG verifier pub struct KZGCommitmentScheme { @@ -235,15 +341,7 @@ where let q_polys = q_polys .iter() - .map(|polys| { - #[cfg(feature = "truncated-challenges")] - let x1 = truncated_powers(x1); - - #[cfg(not(feature = "truncated-challenges"))] - let x1 = powers(x1); - - poly_inner_product(polys, x1) - }) + .map(|polys| poly_inner_product(polys, pcs_powers(x1, polys.len()).into_iter())) .collect::>(); // Sort point sets by ascending cardinality to ensure the first set is the one @@ -283,8 +381,7 @@ where transcript.write(&f_com).map_err(|_| Error::OpeningError)?; let x3: E::Fr = transcript.squeeze_challenge(); - #[cfg(feature = "truncated-challenges")] - let x3 = truncate(x3); + let x3 = pcs_challenge(x3); for q_poly in q_polys.iter() { transcript @@ -297,13 +394,7 @@ where let final_poly = { let mut polys = q_polys; polys.push(f_poly); - #[cfg(feature = "truncated-challenges")] - let powers = truncated_powers(x4); - - #[cfg(not(feature = "truncated-challenges"))] - let powers = powers(x4); - - poly_inner_product(&polys, powers) + poly_inner_product(&polys, pcs_powers(x4, polys.len()).into_iter()) }; let v = eval_polynomial(&final_poly, x3); @@ -390,11 +481,7 @@ where let nb_x1_powers = q_coms.iter().map(|v| v.len()).max().unwrap_or(0); assert!(nb_x1_powers >= q_eval_sets.iter().map(|v| v.len()).max().unwrap_or(0)); - #[cfg(feature = "truncated-challenges")] - let powers_x1 = truncated_powers(x1).take(nb_x1_powers).collect::>(); - - #[cfg(not(feature = "truncated-challenges"))] - let powers_x1 = powers(x1).take(nb_x1_powers).collect::>(); + let powers_x1 = pcs_powers(x1, nb_x1_powers); let q_coms = q_coms .into_iter() @@ -453,8 +540,7 @@ where // Sample a challenge x_3 for checking that f(X) was committed to // correctly. let x3: E::Fr = transcript.squeeze_challenge(); - #[cfg(feature = "truncated-challenges")] - let x3 = truncate(x3); + let x3 = pcs_challenge(x3); #[cfg(feature = "solidity-verifier-trace")] crate::plonk::solidity_trace::record_hashable::(15, "x3", &x3); @@ -492,19 +578,15 @@ where f_com_as_msm.append_term(E::Fr::ONE, f_com, CommitmentLabel::NoLabel); - // Collapse all MSMs before combining with x4 powers, to match the - // in-circuit verifier. Skip the first one since its x4 power is 1. - #[cfg(feature = "truncated-challenges")] - coms.iter_mut().skip(1).for_each(MSMKZG::collapse); + // Collapse all MSMs before combining with truncated x4 powers, to + // match the in-circuit verifier. Skip the first one since its x4 + // power is 1. + if truncated_challenges_enabled() { + coms.iter_mut().skip(1).for_each(MSMKZG::collapse); + } coms.push(f_com_as_msm); - #[cfg(feature = "truncated-challenges")] - let powers = truncated_powers(x4); - - #[cfg(not(feature = "truncated-challenges"))] - let powers = powers(x4); - - msm_inner_product(coms, &powers.take(size).collect::>()) + msm_inner_product(coms, &pcs_powers(x4, size)) }; #[cfg(feature = "solidity-verifier-trace")] crate::plonk::solidity_trace::record_hashable::( @@ -517,13 +599,7 @@ where let mut evals = q_evals_on_x3; evals.push(f_eval); - #[cfg(feature = "truncated-challenges")] - let powers = truncated_powers(x4); - - #[cfg(not(feature = "truncated-challenges"))] - let powers = powers(x4); - - inner_product(&evals, powers) + inner_product(&evals, pcs_powers(x4, evals.len()).into_iter()) }; #[cfg(feature = "solidity-verifier-trace")] { @@ -600,6 +676,22 @@ mod tests { utils::arithmetic::eval_polynomial, }; + #[cfg(feature = "truncated-challenges")] + #[test] + fn scoped_truncated_challenges_overrides_pcs_challenge() { + use ff::Field; + use midnight_curves::Fq; + + let high_bit = Fq::from(2).pow_vartime([200, 0, 0, 0]); + + assert_eq!(super::pcs_challenge(high_bit), Fq::ZERO); + { + let _guard = super::scoped_truncated_challenges(false); + assert_eq!(super::pcs_challenge(high_bit), high_bit); + } + assert_eq!(super::pcs_challenge(high_bit), Fq::ZERO); + } + #[test] fn test_roundtrip_gwc() { use midnight_curves::Bls12; From aef0972b401667e0996ecac301309aa6540b18e7 Mon Sep 17 00:00:00 2001 From: Julien Coolen Date: Tue, 19 May 2026 12:57:46 +0100 Subject: [PATCH 18/19] Redeploy Moonlight Sepolia verifier --- .../Halo2Verifier.runtime.bytecode.hex | 2 +- .../deployments/sepolia/moonlight-wrap/README.md | 14 +++++++------- .../sepolia/moonlight-wrap/deployment.json | 14 +++++++------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.runtime.bytecode.hex b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.runtime.bytecode.hex index 97584913b..1e2afb261 100644 --- a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.runtime.bytecode.hex +++ b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/Halo2Verifier.runtime.bytecode.hex @@ -1 +1 @@ -0x6108e06040526004361015610012575f80fd5b5f3560e01c80631e8e1e1314610078576342b7259714610030575f80fd5b34610074575f366003190112610074576040517f00000000000000000000000080b32f68a333df55da60bec44c498477a8311ede6001600160a01b03168152602090f35b5f80fd5b346100745760403660031901126100745760043567ffffffffffffffff8111610074573660238201121561007457806004013567ffffffffffffffff81116100745760243691830101116100745760243567ffffffffffffffff8111610074573660238201121561007457806004013567ffffffffffffffff8111610074576024369160051b8301011161007457611ec060409114911416156100745760017f00000000000000000000000080b32f68a333df55da60bec44c498477a8311ede803b61428114813f7f24430c63ec2017f12ec4004d66c66314f418dd77fe38dd4c2475c8f2b259e0091416156100745781612700614280923c6013612720511481166014612740511416816127e0511416600b61280051141660076128205114166038612840511416801561007457611e6060443514166013611ec43514163661214414168015610074575f90731a0111ea397fe69a4b1ba7b6434bacd764774b846120a435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa612084351416731a0111ea397fe69a4b1ba7b6434bacd764774b8461206435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa6120443514161680614c70575b15614bdc575b166080616bc061a1c05e808261a24052614bc3575b5f90731a0111ea397fe69a4b1ba7b6434bacd764774b8461212435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa612104351416731a0111ea397fe69a4b1ba7b6434bacd764774b846120e435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa6120c43514161680614ba6575b15614b12575b1680916080616c4061a1c05e61a24052614af9575b80156100745760806127005190525f60a0525f60c0525f60e0525f61010052601361012052610140611ee45b6121448110614ad557508115610074576064906191c0905b826107e48110614ab8575f5160206152095f395f51905f528592607f190160802080608052066169805260a090619940916101008201925b838310614a9a5784835f5160206152095f395f51905f5284607f190160802080608052066169a0525f5160206152095f395f51905f52602060802080608052066169c05260a0619a4061030083015b808410614a78575050619d40608083015b808410614a56575061010060806103f3858095614d96565b948181619e4037019201905b818310614a345750505f5160206152095f395f51905f526080610423838095614d96565b928181619ec0370191607f190160802080608052066169e05260a0610100619f409301925b838310614a165784835f5160206152095f395f51905f5284607f19016080208060805206616a005260a090619fc0916102008201925b8383106149f85784835f5160206152095f395f51905f5284607f19016080208060805206616a205260a090618500610cc08201905b8183106149c75750505f5160206152095f395f51905f528192607f19016080208060805206616a40525f5160206152095f395f51905f5260206080208060805206616a6052610120608061050683614d00565b928181616ac0370191607f190160802092836080526001600160801b035f5160206152095f395f51905f5260a0950616616a8052826182a052015b80821061499f57506080905f5160206152095f395f51905f52611ec493607f1901832080845206616aa05261057581614d00565b508181616b4037010361007457616a205180915f5b6014811061498457506127805192616cc0846127c0515b617060831061495f57505050506105da5f5160206152095f395f51905f525f5160206152695f395f51905f528408918261706052614e2a565b925f5160206152095f395f51905f52616cc092612760519009916127c0515b617060821061493a578585616ce05190616d00915b616e00831061491d575f92611ee4905b61214482106148f857505061706051616cc05190616e005193616cc052616ce052616d0052616d2052616d4052616d6052801561007457616a0051610300525f61a300525f5b61014081106148e957506001805b603182106148c05782618b4051618a40516185205190618a6051916185405161088052618a8051936185605194618aa0516108c0526185805190618ac0516185a0516108a052618ae05191886185c0519586946108805189618b0051905f5160206152095f395f51905f529109905f5160206152095f395f51905f529109955f5160206152095f395f51905f529109936108a0515f5160206152095f395f51905f52910992866108c051905f5160206152095f395f51905f529109925f5160206152095f395f51905f52910990610880515f5160206152095f395f51905f52908c09905f5160206152095f395f51905f528a8c095f5160206152095f395f51905f529108905f5160206152095f395f51905f529108905f5160206152095f395f51905f529108905f5160206152095f395f51905f529108905f5160206152095f395f51905f529108905f5160206152095f395f51905f529108905f5160206152095f395f51905f5291088684618b2051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5291085f5160206152095f395f51905f5290600109956103005161a30051905f5160206152095f395f51905f5291099661a1c051905f5160206152095f395f51905f52910861a1c0526108a0515f5160206152095f395f51905f52035f5160206152095f395f51905f52905f08915f5160206152095f395f51905f52035f5160206152095f395f51905f52905f089061088051905f5160206152095f395f51905f529108905f5160206152095f395f51905f529108905f5160206152095f395f51905f5291085f5160206152095f395f51905f529060010961a1e051905f5160206152095f395f51905f52910861a1e0525f5160206152095f395f51905f52035f5160206152095f395f51905f52905f08915f5160206152095f395f51905f529108905f5160206152095f395f51905f5291085f5160206152095f395f51905f529060010961a20051905f5160206152095f395f51905f529108906185e0515f5160206152095f395f51905f52035f5160206152095f395f51905f52905f089061088051905f5160206152095f395f51905f529108905f5160206152095f395f51905f5291085f5160206152095f395f51905f5290600109918261a9605261030051906103005190610300515f5160206152095f395f51905f529109905f5160206152095f395f51905f529109905f5160206152095f395f51905f52910961a3005261a360515f5160206152095f395f51905f529109905f5160206152095f395f51905f52910861a20052614120905f9061a960825b84906152ef861015611f0357600186515f1a9601959182600514611ed7575081600614611eb75781600814611e9c5781600d14611e725781601014611e4e5781601114611e2a5781602114611b8b5750806019146118405780601f146112ea5780601b14610b2b57600b14610aba575f80fd5b600384519401935f905f5160206152095f395f51905f526103005161a300510961a300525f5160206152095f395f51905f5285611fe08360f31c1661a1c0019283519061ffff8160e81c16610b13575b50089052610a47565b90621fffe0849260e31c1661a3400151900989610b0a565b505082516002909301925f925061a96090839060f01c8015610e575780600114610d2b5780600214610c4657600314610b62575f80fd5b5f5160206152095f395f51905f528080808080618b0051816185e05181035f0890088180618520518161858051918009097f404d21073985d14e432a4ad76d3fae06ca74314b950fe7b1d7f501cd31a8b374099008818061854051816185a051918009097f0b2cc8704264c6bd81bc620e9e524d4b73e9b2317679422ff7fa1603955649f10990088180618560518161862051918009097f0fdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f099008600109808452816103005161a300510961a300528161a2e05161a3605190090861a2e052610a47565b505f5160206152095f395f51905f528080808080618ae051816185c05181035f0890088180618520518161858051918009097f1b8114c381b922fd5d6d241210e2d8a68ad5744053ba9e776118de4107b51ace099008818061854051816185a051918009097f3df32e4cc4cb2ed20e5d21899cf5331775990ccaec4c09b4e3717213fcc0d7630990088180618560518161862051918009097f3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc1099008600109808452816103005161a300510961a300528161a2e05161a3605190090861a2e052610a47565b505f5160206152095f395f51905f528080807f73eda753299d7d483339d80809a1d7f7b67900f7fe6bfad98998021b7bb5732b81807f73eda753299d7d483339d80809a1d80553bca402fffe5bfeffffffff0000000181808080600160701b61852051088180600160701b6185405108600160381b0990088180600160701b6185605108600160701b099008818080618660518161868051600160381b0990086186a0518290600160701b09900881035f08900808818070241eabfffeb153ffffb9feffffffffaaab6187c0510981035f089008088180600160861b817f73eda753299d7d483339d80809a1d80553bda402fffe5b6e855000003ab000026187e051080981035f089008600109808452816103005161a300510961a3005261a240510861a24052610a47565b5061852051610400526185c051610520526185e051610480526186005161044052618540516104a0526187a0516104c0525f5160206152095f395f51905f528080807f73eda753299d7d483339d80809a1d7d340dd972492de594de627fffefcb803498180618560516187805161054052618580516105805261876051610460526185a0516104205261874051610500526186205161056052618640516104e05281808080618660518161868051600160381b0990086186a0518290600160701b09900881035f089181808061044051600160701b09818061048051600160381b096105205108089181808083600160701b0981806104a051600160381b09610400510808918180806104c0516104e05109700c8557e86f90d0d89eed6eb5349a0f88200991818080610540516104e05109702cb9b546d20373eaf85e8f53db883cb5480991818080610460516104e0510970013af65741744bd7bb2c6872df2b8003200991818080610500516104e0510970340f2ebe380a0f5eff4360543988a61dc20991818080610440516104e0510970297784894e27525bc342b7fde37dba93660991818080610480516104e05109703212e00cde6d2002b119d800000347fcb809918180806104c0516105605109702cb9b546d20373eaf85e8f53db883cb548099181808061054051610560510970013af65741744bd7bb2c6872df2b800320099181808061046051610560510970340f2ebe380a0f5eff4360543988a61dc2099181808061050051610560510970297784894e27525bc342b7fde37dba93660991818080610440516105605109703212e00cde6d2002b119d800000347fcb809918180806104c051610420510970013af65741744bd7bb2c6872df2b800320099181808061054051610420510970340f2ebe380a0f5eff4360543988a61dc2099181808061046051610420510970297784894e27525bc342b7fde37dba93660991818080610500516104205109703212e00cde6d2002b119d800000347fcb809918180806104c051610580510970340f2ebe380a0f5eff4360543988a61dc2099181808061054051610580510970297784894e27525bc342b7fde37dba93660991818080610460516105805109703212e00cde6d2002b119d800000347fcb809918180806104c051840970297784894e27525bc342b7fde37dba936609918180808080610540518609703212e00cde6d2002b119d800000347fcb80993610520519009600160701b098180806104c0516104a05109703212e00cde6d2002b119d800000347fcb809818080610480516104a05109600160701b09818080610520516104a05109600160381b09818080610440516104005109600160701b09818080610480516104005109600160381b09816105205161040051090808080808080808080808080808080808080808080808080808080808818070241eabfffeb153ffffb9feffffffffaaab6187c0510981035f089008088180600160861b817f73eda753299d7d483339d80809a1d80553b9202d7ffe85d4800008bb200000016187e051080981035f089008600109808452816103005161a300510961a3005261a220510861a22052610a47565b505090505f905f61a96090616d40516103a052616d005161032052616d20516103e0526169a05161038052616980516103c0525f5160206152095f395f51905f52806190e05181610320516103a0510809816103005161a30051090861a300525f5160206152095f395f51905f52618b6051816103c05191816103c0515f0908095f5b6004811061181357505060015f5b600481106117f15750600161a9e05260015b600481106117bf5750600161aac0526003805b61178d57505f905f5b600481106117605750905f5160206152095f395f51905f52808080808096816190c05197810391880908816103005161a3005109089381808080618be051948180618b8051816103c0515f090881618ba051916103c05190090895096190e0510881036191005108816190a0519361038051900890090881806103e05161032051088103600108099161030051900908610360526191605161034052618a00516102e05261852051610260526185405161028052618560516102c052618580516102a0526185a0516102405261862051610220526186405161020052618660516101e052618680516101c0526186a0516101a0526186c051610180526186e0516101605261870051610140526187205161012052618bc051610100526191405160e0525f5160206152095f395f51905f5280618c005181035f0860010860c0525f5160206152095f395f51905f52808060e05160010961034051088103619180510860a0525f5160206152095f395f51905f5280806191205181806103805181806101005160c05109816103c05181806101205160c05109816103c05181806101405160c05109816103c05181806101605160c05109816103c05181806101805160c05109816103c05181806101a05160c05109816103c05181806101c05160c05109816103c05181806101e05160c05109816103c05181806102005160c05109816103c05181806102205160c05109816103c05181806102405160c05109816103c05181806102a05160c05109816103c05181806102c05160c05109816103c05181806102805160c05109816103c05181806102605160c05109816103c05181806102e05160c05109816103c0515f09080908090809080908090809080908090809080908090809080908090809080860a051090881806103e0516103205108810360010809816103005181805f5160206152695f395f51905f528180610380518161010051816103c0518161012051816103c0518161014051816103c0518161016051816103c0518161018051816103c051816101a051816103c051816101c051816103c051816101e051816103c0518161020051816103c0518161022051816103c0518161024051816103c051816102a051816103c051816102c051816103c0518161028051816103c0518161026051816103c051816102e051816103c0515f09080908090809080908090809080908090809080908090809080908090809080860e0510908816103005181806103405181610320516103a051080981610300516103605109080908090861a30052610a47565b915f5160206152095f395f51905f52600191818560051b8061aa6001519061a9e0015109900892016113a9565b5f5160206152095f395f51905f528160051b808601519061aa600151095f19820160051b61aa6001525f1901806113a0565b6001905f5160206152095f395f51905f525f19820160051b808701519061a9e00151098160051b61a9e001520161138d565b905f5160206152095f395f51905f526001918360051b8601519009910161137b565b8060019160051b5f5160206152095f395f51905f526103805181836185400151870808908601520161136d565b5050618a205161a9609081525f925082805b60058110611b7257506185005161aa2052616d605161aa40525f5b60098110611b595750618a005161ab80525f5b60128110611b4057505f5b60068110611b2557505f5b60068110611b0a57505f5b60058110611aef57505f5160206152095f395f51905f5280808061ade0518103600108616d405109816103005161a30051090881808061ae80518181810391800908616d005109916103005190090861a3005260015b60068110611aa957505f5160206152095f395f51905f52616a20516169a0510961b000525f5b6006811061192b5750610a47565b600381026003810160128111611aa1575b8260051b908161aea001519161ade001519061b00051908194906169c051906169a0515b8a8285106119d457505050505050600193925f5160206152095f395f51905f52808080957f4285088329c399ea457a8ca1d30f8957e74c7f529842a1579b4fee55b398292395820390088180616d2051616d0051088103890809816103005161a30051090861a300520961b000520161191d565b9285979385969792939482809760051b809301519261aba001515f5160206152095f395f51905f529087095f5160206152095f395f51905f52908408905f5160206152095f395f51905f5291085f5160206152095f395f51905f529109985f5160206152095f395f51905f529108905f5160206152095f395f51905f5291085f5160206152095f395f51905f529109947f08634d0aa021aaf843cab354fabb0062f6502437c6a09c006c083479590189d75f5160206152095f395f51905f52910993600101929190611960565b50601261193c565b805f5160206152095f395f51905f52808060019460051b61ade001515f19850160051b61af60015182039008616d405109816103005161a30051090861a30052016118f7565b80606060019202618ec001518160051b61af600152016118a1565b80606060019202618ea001518160051b61aea0015201611896565b80606060019202618e8001518160051b61ade001520161188b565b8060019160051b80618c4001519061aba0015201611880565b8060019160051b8061862001519061aa6001520161186d565b8060019160051b8061852001519061a980015201611852565b92939480915090600181515f1a91015f9260018316611e1a575b505f9360028316611e01575b815196875f1a8860011a908960021a9a60058b60041a960199611df3575b505f5b818110611da65750505f5b818110611d475750505f5b878a821015611c7f5788519860118a60f01c9101995f5b60078110611c135750505050600101611be8565b8060051b8301515f6080525b600760805110611c325750600101611bff565b9a5f5160206152095f395f51905f5290818d81600460805187018a0101515f1a60051b612ae001519160805160051b61ffff8960e01c16015190090990089a600160805101608052611c1f565b5050959750955f905b8060031a8210611d0d5750505f905b808210611cc9575050600116611cb1575b50916001610a47565b905f5160206152095f395f51905f5291510984611ca8565b90935f5160206152095f395f51905f526001918160058b519b019a81815f1a60051b612ae001519161ffff808260d81c16519160e81c165109099008940190611c97565b90945f5160206152095f395f51905f526001918160038c519c019b61ffff8160e81c1651905f1a60051b612ae00151099008950190611c88565b6002895160f01c990198515f905b8a60078310611d6957505050600101611bdd565b600191929a5f5160206152095f395f51905f5260038193519e019d8161ffff825f1a60051b612ae001519260e81c16518709099008990190611d55565b5f5b8a60078210611dbb575050600101611bd2565b6001919a5f5160206152095f395f51905f5260038193519e019d61ffff8160e81c1651905f1a60051b612ae001510990089901611da8565b84526020909301928b611bcf565b9350600181515f1a91019060051b612ae0015193611bb1565b905160f01c925060030187611ba5565b935f5160206152095f395f51905f5291506002865160f01c96019551900992610a47565b935f5160206152095f395f51905f5291506002865160f01c96019551900892610a47565b935f5160206152095f395f51905f529150600186515f1a96019560051b612ae00151900992610a47565b935f5160206152095f395f51905f52809250035f0892610a47565b93915f5160206152095f395f51905f529150601f19019182510892610a47565b92949150946003905160f01c920194611ef5575b5051916001610a47565b835260209092019184611eeb565b83906152ef870361007457610074576169e051610840525f5160206152095f395f51905f52618ae051815f5160206152695f395f51905f526185c051099008610720526185205161082052618540516108005261856051610760525f5160206152095f395f51905f526107605161076051096107e05261858051610860525f5160206152095f395f51905f52610860516108605109610740526185a0516107a0525f5160206152095f395f51905f526107a0516107a051096107805261862051610700525f5160206152095f395f51905f526107005161070051096107c052618640516106e0525f5160206152095f395f51905f526106e0516106e051096106c052618660516106a0525f5160206152095f395f51905f526106a0516106a05109610680525f5160206152095f395f51905f52618b0051815f5160206152695f395f51905f526185e05109900861066052618b205161064052618b405161062052618a405161060052618a60516105e052618a80516105c0525f5160206152095f395f51905f52618aa051815f5160206152695f395f51905f52618600510990086105a0525f5160206152095f395f51905f5280808080618c205181036001086191a0519009810381808080806106805161068051096106a051095f5160206152295f395f51905f5209818080806106c0516106c051096106e051095f5160206152895f395f51905f5209818080806107c0516107c0510961070051095f5160206151e95f395f51905f5209818080806107805161078051096107a051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e098180808061074051610740510961086051097ed94c46a8456352aa44d7a885ab59e3a36664e6fb25e826f8a4cd79822f053309818080806107e0516107e0510961076051097f1f61345b652161410c5e29f51e301ae56342af824bc110649393d2b911c50d3e098180610800517f40fa389feb2522bb934881ac9ed749aee2296502af592418c6b5675c0f560261098180610820517f70d8f2a733a64d650faccc9b1c2a766a9544bb3ff1a11ee73cb43947ef386633096105a0510808080808080808816108405181808080806106c0516106c051096106e051095f5160206152295f395f51905f5209818080806107c0516107c0510961070051095f5160206152895f395f51905f5209818080806107805161078051096107a051095f5160206151e95f395f51905f52098180808061074051610740510961086051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e09818080806107e0516107e0510961076051097ed94c46a8456352aa44d7a885ab59e3a36664e6fb25e826f8a4cd79822f0533098180610800517f27e7119226c42a6d19c1541904b99ae40685511ed2e078964b74594d38340849098180610820517f6d05a41959f539a7fc9ec0972ea1e3dbb6fc67dd51daf3414f7fbbb091c7274a0981805f5160206152695f395f51905f526106a051096105c0510808080808080808816108405181808080806107c0516107c0510961070051095f5160206152295f395f51905f5209818080806107805161078051096107a051095f5160206152895f395f51905f52098180808061074051610740510961086051095f5160206151e95f395f51905f5209818080806107e0516107e0510961076051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e098180610800517f23a6684b942d726a22e4d5b8d8ff83aeaa773f62600184efe5d033d7c7c6e827098180610820517f2f5908b169c6cf1bd26dcf0f9e5105481f5164f3ece0582bf3098312167751a70981805f5160206152695f395f51905f526106e051096105e05108080808080808816108405181808080806107805161078051096107a051095f5160206152295f395f51905f52098180808061074051610740510961086051095f5160206152895f395f51905f5209818080806107e0516107e0510961076051095f5160206151e95f395f51905f52098180610800517f24822e1af9aa2887c912c87eb0f20bd332330e7e55cd784de67cb407a9f05520098180610820517f726df1506749848155630b86ae25a82b281ecd050fe3a52d85a181fa87202e4b0981805f5160206152695f395f51905f526107005109610600510808080808088161084051818080808061074051610740510961086051095f5160206152295f395f51905f5209818080806107e0516107e0510961076051095f5160206152895f395f51905f52098180610800517f26c2cc87f95726b28f33ca03409a460ec987cfe12adae32769e3565865d07191098180610820517f222e83e70453dfee19b402e9fa8dfe2c4987b034d0be3ceb478b3022e97934c10981805f5160206152695f395f51905f526107a05109610620510808080808816108405181808080806107e0516107e0510961076051095f5160206152295f395f51905f52098180610800517f6bd72f9cfc53af9d931896e77ea5c61244cb6d5fae8954f37dc7b9002f5aa78a098180610820517f5e1d3dbecda6214343e24a47f45c5d033197ad01b65a730af95dc57e90c491400981805f5160206152695f395f51905f5261086051096106405108080808816108405181808080806106805161068051096106a051097f0fdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f09818080806106c0516106c051096106e051097f301cf56f9b4577112cc4241cddf6484aaadedbf1bbd0f2351adf2e41c2fb2ecd09818080806107c0516107c0510961070051097f5f3a15bab4ce4097b1edc3a25002694b92395ce355a8a12fe557459d9633f70109818080806107805161078051096107a051097f275a20361ea91992193920270d3e2d1f6361880ac0a439c64bef815d4469ba85098180808061074051610740510961086051097f31e823a45e567484c1544e310c0fa5cd66547a8f0dde659ac61698c30e838d2509818080806107e0516107e0510961076051097f26cc223e16f47c20e17cc6069605fa5a8af05ea4f6eb36029a641d23b818eb10098180610800517f4d0ea7f9c3fda06d9535b0fdafd8338bd47c2200b284fa71a325ff41ac358028098180610820517f5b1fc262a28cbb8bf75d9b1a6edaa74591ec24cd9a209512213cec3a3c0f1a5d09610660510808080808080808816108405181808080806106805161068051096106a051097f3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc109818080806106c0516106c051096106e051097f06ccb1c7d87f3c12a2bde4e68ac7f1e8b03481ba15d7f88f9a7f9b8310dd6d3409818080806107c0516107c0510961070051097f53fded36d490ba6b05a5d10fd99ffe5456baec6a6a8753199d5ebdc33c99790e09818080806107805161078051096107a051097f412c98232b6ab8a47aa76ee814ef7ec6261987c9802f2cfc490e007951a60ca5098180808061074051610740510961086051097f333f8046ece5579cbd6872449c57f2703dfc8864cfadc06d587ff104a0d0c1f209818080806107e0516107e0510961076051097f3509dd2fe3aac0080783557fec090fb1cb4b2b0901253c55282024331d1fe1a8098180610800517f52f789e4afc3801f7411102ee2f47cc5954a744e71cac98e75ea962a55a0a76f098180610820517f590ba402032e82eb1f660ef09796c5686345a5054ed96dae8e2d2336337887710961072051080808080808080881610840515f0908090809080908090809080908090808816103005161a3005109088161a9405161a1c0510961a1c0528161a9205161a1e0510961a1e0528161a8c05161a200510961a200528161a8605161a220510961a220528161a8005161a240510961a240528161a7a05161a260510961a260528161a7405161a280510961a280528161a6e05161a2a0510961a2a0528161a6805161a2c0510961a2c0528161a5c05161a2e0510961a2e05281035f08616d8052616a2051908160015f5b6014811061489d57505f5160206152095f395f51905f5280808080888180988198616da0528103600108616dc05281808061278051816127a051809c81809c81809c818c819d9b829c9a839b6170405282096170605209806170205209090909090909090961700052616a4051600161738052600190617380905f915b602a83106148715784618a0061a3005261850061a320526190a061a340526190c061a3605261912061a3805261914061a3a0526191a061a3c052618a2061a3e052618a4061a40052618a6061a42052618a8061a44052618aa061a46052618ac061a48052618ae061a4a052618b0061a4c052618b2061a4e052618b4061a50052618b6061a52052618b8061a54052618ba061a56052618bc061a58052618be061a5a052618c0061a5c052618c2061a5e052618c4061a60052618c6061a62052618c8061a64052618ca061a66052618cc061a68052618ce061a6a052618d0061a6c052618d2061a6e052618d4061a70052618d6061a72052618d8061a74052618da061a76052618dc061a78052618de061a7a052618e0061a7c052618e2061a7e052618e4061a80052618e6061a82052616d8061a84052618a00516173a061a32060015b602b811061484657505050617ba0526186e0515f5160206152095f395f51905f526189a0519181806173a051928184618700510990089381836189c05109900882806173c051958187618720510990089181866189e05109900890617bc052617be052818080619060518180619080519281886190e051099008956191005109900892818661916051099008936191805109900890617c0052617c205261852061a300526185c061a3205261884061a3405261854061a360526185e061a3805261886061a3a05261856061a3c05261860061a3e05261888061a4005261858061a4205261874061a440526188a061a460526185a061a4805261876061a4a0526188c061a4c05261862061a4e05261878061a500526188e061a5205261864061a540526187a061a5605261890061a5805261866061a5a0526187c061a5c05261892061a5e05261868061a600526187e061a6205261894061a640526186a061a6605261880061a6805261896061a6a0526186c061a6c05261882061a6e05261898061a70052618520516185c05161884051916173a061a36060015b600b81106147fd57505050617c4052617c6052617c8052618e8061a30052618ea061a32052618ec061a34052618ee061a36052618f0061a38052618f2061a3a052618f4061a3c052618f6061a3e052618f8061a40052618fa061a42052618fc061a44052618fe061a4605261900061a4805261902061a4a05261904061a4c052618e8051618ea051618ec051916173a061a36060015b600581106147b457505050617ca052617cc052617ce052616a6051616a80516182a0516170005191836170205181617040519581617060519188815f5160206152095f395f51905f52035f5160206152095f395f51905f529089085f5160206152095f395f51905f528581038a085f5160206152095f395f51905f528381038b08905f5160206152095f395f51905f529109905f5160206152095f395f51905f529109915f5160206152095f395f51905f5281810383085f5160206152095f395f51905f5286810384085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52908409895f5160206152095f395f51905f5283810388085f5160206152095f395f51905f5285810389085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5290830994878d5f5160206152095f395f51905f5282810387085f5160206152095f395f51905f5288810388085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5290890961303990614c8d565b955f5160206152095f395f51905f5283810382085f5160206152095f395f51905f5289810383085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52908809935f5160206152095f395f51905f5282810385085f5160206152095f395f51905f528a810386085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f529086095f5160206152095f395f51905f528381038b085f5160206152095f395f51905f528681038c085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52908209995f5160206152095f395f51905f5286810389085f5160206152095f395f51905f528281038a08905f5160206152095f395f51905f5291095f5160206152095f395f51905f52908c099a8b945f5160206152095f395f51905f52035f5160206152095f395f51905f52908a085f5160206152095f395f51905f529109905f5160206152095f395f51905f52035f5160206152095f395f51905f529089085f5160206152095f395f51905f529082099788965f5160206152095f395f51905f52035f5160206152095f395f51905f5291085f5160206152095f395f51905f529109915f5160206152095f395f51905f52910981617ca051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5203935f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52916080013509905f5160206152095f395f51905f529108925f5160206152095f395f51905f52910990617cc051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52035f5160206152095f395f51905f529108925f5160206152095f395f51905f52910990617ce051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52035f5160206152095f395f51905f5291085f5160206152095f395f51905f52825f09905f5160206152095f395f51905f52910887895f5160206152095f395f51905f5285810388085f5160206152095f395f51905f5282810389085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f528881038b085f5160206152095f395f51905f528781038c085f5160206152095f395f51905f528481038d08905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291098a5f5160206152095f395f51905f528a810385085f5160206152095f395f51905f5289810386085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5290830991885f5160206152095f395f51905f528c810382085f5160206152095f395f51905f5287810383085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52908509968c5f5160206152095f395f51905f52878a0961350090614c8d565b965f5160206152095f395f51905f52908809935f5160206152095f395f51905f5282810385085f5160206152095f395f51905f528a810386085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f529086095f5160206152095f395f51905f528381038b085f5160206152095f395f51905f528681038c085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52908209995f5160206152095f395f51905f5286810389085f5160206152095f395f51905f528281038a08905f5160206152095f395f51905f5291095f5160206152095f395f51905f52908c099a8b945f5160206152095f395f51905f52035f5160206152095f395f51905f52908a085f5160206152095f395f51905f529109905f5160206152095f395f51905f52035f5160206152095f395f51905f529089085f5160206152095f395f51905f529082099788965f5160206152095f395f51905f52035f5160206152095f395f51905f5291085f5160206152095f395f51905f529109915f5160206152095f395f51905f52910981617c4051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5203935f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52916060013509905f5160206152095f395f51905f529108925f5160206152095f395f51905f52910990617c6051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52035f5160206152095f395f51905f529108925f5160206152095f395f51905f52910990617c8051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52035f5160206152095f395f51905f529108915f5160206152095f395f51905f529109905f5160206152095f395f51905f529108905f5160206152095f395f51905f5281810389085f5160206152095f395f51905f52906001095f5160206152095f395f51905f5282810388085f5160206152095f395f51905f528a81038908905f5160206152095f395f51905f529109905f5160206152095f395f51905f529109905f5160206152095f395f51905f5289810382085f5160206152095f395f51905f52906001095f5160206152095f395f51905f529083096138b390614c8d565b5f5160206152095f395f51905f528a810383085f5160206152095f395f51905f52906001095f5160206152095f395f51905f52908209915f5160206152095f395f51905f528181038c085f5160206152095f395f51905f52906001095f5160206152095f395f51905f52908409905f5160206152095f395f51905f528c81038b085f5160206152095f395f51905f529083099384925f5160206152095f395f51905f528381038d085f5160206152095f395f51905f529109915f5160206152095f395f51905f52035f5160206152095f395f51905f52908c085f5160206152095f395f51905f528e81038d08905f5160206152095f395f51905f5291095f5160206152095f395f51905f52910981617c0051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5203915f5160206152095f395f51905f5291095f5160206152095f395f51905f529060408c013509905f5160206152095f395f51905f529108925f5160206152095f395f51905f52910990617c2051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52035f5160206152095f395f51905f529108915f5160206152095f395f51905f529109905f5160206152095f395f51905f529108905f5160206152095f395f51905f5281810387085f5160206152095f395f51905f52906001095f5160206152095f395f51905f5282810386085f5160206152095f395f51905f528881038708905f5160206152095f395f51905f5291095f5160206152095f395f51905f52828209925f5160206152095f395f51905f5289810382085f5160206152095f395f51905f52906001095f5160206152095f395f51905f52908509613b5490614c8d565b915f5160206152095f395f51905f528a810383085f5160206152095f395f51905f52906001095f5160206152095f395f51905f52908409935f5160206152095f395f51905f52908509935f5160206152095f395f51905f528b81038a085f5160206152095f395f51905f529086099485935f5160206152095f395f51905f52035f5160206152095f395f51905f52908b085f5160206152095f395f51905f529109915f5160206152095f395f51905f52910981617bc051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5203915f5160206152095f395f51905f5291095f5160206152095f395f51905f529060208a013509905f5160206152095f395f51905f529108925f5160206152095f395f51905f52910990617be051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52035f5160206152095f395f51905f529108915f5160206152095f395f51905f529109905f5160206152095f395f51905f529108925f5160206152095f395f51905f52035f5160206152095f395f51905f529108613d1590614c8d565b90617ba0515f5160206152095f395f51905f52039035905f5160206152095f395f51905f529108905f5160206152095f395f51905f529109915f5160206152095f395f51905f529109905f5160206152095f395f51905f5291089081616e4052616aa05180808094616da051616dc051916182a051925f5160206152095f395f51905f52856001096001600160801b038116955f5160206152095f395f51905f5291096001600160801b038116965f5160206152095f395f51905f5291096001600160801b038116995f5160206152095f395f51905f5291096001600160801b038116975f5160206152095f395f51905f5291096001600160801b03169260806198c061a3005e600161a38052608061994061a3a05e6173c05161a420526080619d4061a4405e6173e05161a4c05260806199c061a4e05e6174005161a560526080619dc061a5805e6174205161a600526080619f4061a6205e6174405161a6a052608061578061a6c05e6174605161a74052608061550061a7605e6174805161a7e052608061558061a8005e6174a05161a88052608061560061a8a05e6174c05161a92052608061568061a9405e6174e05161a9c052608061570061a9e05e6175005161aa6052608061530061aa805e6175205161ab0052608061538061ab205e6175405161aba052608061540061abc05e6175605161ac4052608061548061ac605e6175805161ace052608061580061ad005e6175a05161ad8052608061588061ada05e6175c05161ae2052608061590061ae405e6175e05161aec052608061598061aee05e6176005161af60526080615b8061af805e6176205161b000526080615f0061b0205e6176405161b0a052608061600061b0c05e6176605161b14052608061608061b1605e6176805161b1e052608061610061b2005e6176a05161b28052608061618061b2a05e6176c05161b32052608061620061b3405e6176e05161b3c052608061628061b3e05e6177005161b46052608061630061b4805e6177205161b50052608061638061b5205e6177405161b5a052608061640061b5c05e6177605161b64052608061648061b6605e6177805161b6e052608061650061b7005e6177a05161b78052608061658061b7a05e6177c05161b82052608061660061b8405e6177e05161b8c052608061668061b8e05e6178005161b96052608061670061b9805e6178205161ba0052608061678061ba205e6178405161baa052608061680061bac05e6178605161bb4052608061688061bb605e6178805161bbe052608061690061bc005e6178a05161bc80526178c051915f5160206152095f395f51905f529083096080619fc061bca05e818161bd20525f5160206152095f395f51905f529109608061a04061bd405e818161bdc0525f5160206152095f395f51905f52910990608061a0c061bde05e8161be6052608061a14061be805e5f5160206152095f395f51905f52910961bf00526080615a0061bf205e61a1c0515f5160206152095f395f51905f5290820961bfa0526080615a8061bfc05e61a1e0515f5160206152095f395f51905f5290820961c040526080615b0061c0605e61a200515f5160206152095f395f51905f5290820961c0e0526080615c0061c1005e61a220515f5160206152095f395f51905f5290820961c180526080615c8061c1a05e61a240515f5160206152095f395f51905f5290820961c220526080615d0061c2405e61a260515f5160206152095f395f51905f5290820961c2c0526080615d8061c2e05e61a280515f5160206152095f395f51905f5290820961c360526080615e0061c3805e61a2a0515f5160206152095f395f51905f5290820961c400526080615e8061c4205e61a2c0515f5160206152095f395f51905f5290820961c4a0526080615f8061c4c05e61a2e0515f5160206152095f395f51905f52910961c54052608061974061c5605e8361c5e05260806197c061c6005e836173a051905f5160206152095f395f51905f52910961c68052608061984061c6a05e836173c051905f5160206152095f395f51905f52910961c720526080619cc061c7405e8461c7c0526080619e4061c7e05e846173a051905f5160206152095f395f51905f52910961c860526080619ec061c8805e846173c051905f5160206152095f395f51905f52910961c9005260806191c061c9205e8761c9a052608061924061c9c05e876173a051905f5160206152095f395f51905f52910961ca405260806192c061ca605e876173c051905f5160206152095f395f51905f52910961cae052608061934061cb005e876173e051905f5160206152095f395f51905f52910961cb805260806193c061cba05e8761740051905f5160206152095f395f51905f52910961cc2052608061944061cc405e8761742051905f5160206152095f395f51905f52910961ccc05260806194c061cce05e8761744051905f5160206152095f395f51905f52910961cd6052608061954061cd805e8761746051905f5160206152095f395f51905f52910961ce005260806195c061ce205e8761748051905f5160206152095f395f51905f52910961cea052608061964061cec05e876174a051905f5160206152095f395f51905f52910961cf405260806196c061cf605e876174c051905f5160206152095f395f51905f52910961cfe0526080619a4061d0005e8561d080526080619ac061d0a05e856173a051905f5160206152095f395f51905f52910961d120526080619b4061d1405e856173c051905f5160206152095f395f51905f52910961d1c0526080619bc061d1e05e856173e051905f5160206152095f395f51905f52910961d260526080619c4061d2805e8561740051905f5160206152095f395f51905f52910961d300526080616ac061d3205e868261d3a05261478d575b5f5160206152095f395f51905f5296979387809693948180808080809a8199099c60808601350999606085013509966040840135099360208301350990350808080808616e60526080616b40616f005e6080612860815e805f5160206152095f395f51905f52616e605181035f0861010052614778575b806080616e806101005e614762575b6080616b406101005e80616a80516101805261474b575b80614735575b608080616f805e7c70616972696e672d62617463682d6163632d6b7a670000000000000000610100526080616f806101205e6080616f006101a05e6080616c406102205e6080616bc06102a05e5f5160206152095f395f51905f52610220610100200690811561472c575b6080616c406101005e808261018052614715575b806080616f806101805e6146fd575b80916080616bc06101005e610180526146e6575b806080616f006101805e6146ce575b8015610074576146c290614f51565b50600160805260206080f35b506080616f0061010080600b5afa60803d14166146b3565b50608061010060a081600c5afa60803d14166146a4565b506080616f8061010080600b5afa60803d1416614690565b50608061010060a081600c5afa60803d1416614681565b6001915061466d565b5060808061010081600b5afa60803d1416614602565b50608061010060a081600c5afa60803d14166145fc565b5060808061010081600b5afa60803d14166145e5565b5060808060a081600c5afa60803d14166145d6565b9692955090925a616e806130c061a300600c608094fa3d608014169592969391909361455f565b909194606060205f5160206152095f395f51905f526001938180808c9b9a9b519b818d8c515109900899818c878c01515109900899604089015151099008970193019101612e7a565b909194606060205f5160206152095f395f51905f526001938180808c9b9a9b519b818d8c515109900899818c878c01515109900899604089015151099008970193019101612de4565b9091926020805f5160206152095f395f51905f52600193818851885151099008950193019101612c4a565b5f5160206152095f395f51905f52826020600193019509926001600160801b0384168552019192612b07565b91905f5160206152095f395f51905f528086818460019509099280099201612a8a565b5f5160206152095f395f51905f5260019161030051900991828160051b61a34001520190610672565b5f61a1c0820152602001610664565b909360205f5160206152095f395f51905f52819281883586510990089501910161061e565b5f5160206152095f395f51905f526020918451900892019161060e565b5f5160206152095f395f51905f528382828060209587510988098552099101906105f9565b602091815f5160206152095f395f51905f5280938103870885520991019085906105a1565b915f5160206152095f395f51905f528160019209920161058a565b9181355f5160206152095f395f51905f52811015610074578152602090810192910190610541565b90928235915f5160206152095f395f51905f52831015610074578281529181526020908101939281019291016104b3565b91906080614a07838293614d96565b9381848237019101919061047e565b91906080614a25838293614d96565b93818482370191019190610448565b9190926080614a44838293614d96565b938184823701910192909291926103ff565b916080614a67858293969496614d96565b9481848237019101919291926103db565b916080614a89858293969496614d96565b9481848237019101919291926103ca565b91906080614aa9838293614d96565b9381848237019101919061037b565b614ac6608092918392614d96565b92818582370192019190610343565b90602080918335945f5160206152095f395f51905f5286101694815201910161032b565b506080616c4060a061a1c0600c5afa60803d14166102ff565b9050614b28600160381b600760386120c4615030565b9392614b3e600160381b60076038612104615179565b5093919092169580614b82575b15614b5a575b505050506102ea565b909192948383178287171715151694616c4052616c6052616c8052616ca05283808080614b51565b95838317828617171516955f616c40525f616c60525f616c80525f616ca052614b4b565b915082915f616c40525f616c60525f616c80525f616ca0526102e4565b506080616bc060a061a1c0600c5afa60803d1416610261565b9050614bf2600160381b60076038612044615030565b9392614c08600160381b60076038612084615179565b5093919092169580614c4c575b15614c24575b5050505061024c565b909192948383178287171715151694616bc052616be052616c0052616c205283808080614c1b565b95838317828617171516955f616bc0525f616be0525f616c00525f616c2052614c15565b915082915f616bc0525f616be0525f616c00525f616c2052610246565b801561007457602061260052602061262052602061264052612660527f73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffffff612680525f5160206152095f395f51905f526126a052602061260060c08160055afa156100745760203d03610074576126005190565b80356040820135908060801c610074578160801c610074576001600160801b038091169116906f1a0111ea397fe69a4b1ba7b6434bacd75f5160206152495f395f51905f52602085013511158183141691101715610074576f1a0111ea397fe69a4b1ba7b6434bacd75f5160206152495f395f51905f526060840135111581831416911017156100745760809060a03761012090565b81356040830135908060801c610074578160801c610074576001600160801b038091169116906f1a0111ea397fe69a4b1ba7b6434bacd75f5160206152495f395f51905f52602086013511158183141691101715610074576f1a0111ea397fe69a4b1ba7b6434bacd75f5160206152495f395f51905f52606085013511158183141691101715610074576080809282370190565b8015614f4e575061a1c090616cc05191616ce0925b6170608410614f295783515f5160206152095f395f51905f5291098015614f2257602082526020808301526020604083015260608201527f73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffffff60808201525f5160206152095f395f51905f5260a082015260208160c08160055afa60203d141692815191601f1901905b80616ce010614efa5750505f5160206152095f395f51905f5280616ce051830991616cc051900990616cc052616ce052565b5f5160206152095f395f51905f5280835185099382519009928152601f199182019101614ec8565b505f925050565b60205f5160206152095f395f51905f5281928694965190099283865201930190614e3f565b90565b8015614f4e57506080616f806103005e6101006128e06103805e6080616f006104805e6101006129e06105005e60206103008080600f5afa60203d141661030051161561007457600190565b5f96945f969293945f1901925f5b868110614fbb5750505050505050565b8483820483808260051b880135921516615028575b5087858406021c16868202610100811080615007575b15614ff6575b5050600101614fab565b60ff19011b9099019860015f614fec565b9a82821b019a6101008983011115614fe6579b8282610100031c019b614fe6565b900383614fd0565b600193925f926003820160021c845b81811061513357505084833510156001166150ec575b93600491849582615067960294614f9d565b8192919381936f1a0111ea397fe69a4b1ba7b6434bacd781145f5160206152495f395f51905f5284148116806150e1575b156150d1575b6f1a0111ea397fe69a4b1ba7b6434bacd7905f5160206152495f395f51905f52600160801b891095111516911017161693565b600186019586109096019561509e565b5f9750879650615098565b936150679350815f5160206152495f395f51905f526f1a0111ea397fe69a4b1ba7b6434bacd76151228460048181988c8b614f9d565b929092149114169450915093615055565b828160021b8503600490818110615171575b5002610100811061515a575b5060010161503f565b9760018092991b8960051b87013510169790615151565b90505f615145565b9290915f926001946003830160021c5f5b8181106151a2575050925f9260049261506795614f9d565b838160021b86036004908181106151e0575b500261010081106151c9575b5060010161518a565b9760018092991b8960051b850135101697906151c0565b90505f6151b456fe4e5280109d8f96b8bfb543a6b1af25fb56a9db616af85a90eedc558e3eb1ea2973eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff000000014997c5aa3a5fa07bcaf880a9054bef831effbd9cd58e46d9bb4fb88ef99de0db64774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff000000004382d0938a760120dd6cef8f3b90a0c38abae475e3d21e39365472b76d780272 +0x6108e06040526004361015610012575f80fd5b5f3560e01c80631e8e1e1314610078576342b7259714610030575f80fd5b34610074575f366003190112610074576040517f0000000000000000000000008c70f334be6ba4bd13bb0a4f7a88fa640a9a09106001600160a01b03168152602090f35b5f80fd5b346100745760403660031901126100745760043567ffffffffffffffff8111610074573660238201121561007457806004013567ffffffffffffffff81116100745760243691830101116100745760243567ffffffffffffffff8111610074573660238201121561007457806004013567ffffffffffffffff8111610074576024369160051b8301011161007457611ec060409114911416156100745760017f0000000000000000000000008c70f334be6ba4bd13bb0a4f7a88fa640a9a0910803b61428114813f7f24430c63ec2017f12ec4004d66c66314f418dd77fe38dd4c2475c8f2b259e0091416156100745781612700614280923c6013612720511481166014612740511416816127e0511416600b61280051141660076128205114166038612840511416801561007457611e6060443514166013611ec43514163661214414168015610074575f90731a0111ea397fe69a4b1ba7b6434bacd764774b846120a435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa612084351416731a0111ea397fe69a4b1ba7b6434bacd764774b8461206435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa6120443514161680614c70575b15614bdc575b166080616bc061a1c05e808261a24052614bc3575b5f90731a0111ea397fe69a4b1ba7b6434bacd764774b8461212435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa612104351416731a0111ea397fe69a4b1ba7b6434bacd764774b846120e435147bf38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa6120c43514161680614ba6575b15614b12575b1680916080616c4061a1c05e61a24052614af9575b80156100745760806127005190525f60a0525f60c0525f60e0525f61010052601361012052610140611ee45b6121448110614ad557508115610074576064906191c0905b826107e48110614ab8575f5160206152095f395f51905f528592607f190160802080608052066169805260a090619940916101008201925b838310614a9a5784835f5160206152095f395f51905f5284607f190160802080608052066169a0525f5160206152095f395f51905f52602060802080608052066169c05260a0619a4061030083015b808410614a78575050619d40608083015b808410614a56575061010060806103f3858095614d96565b948181619e4037019201905b818310614a345750505f5160206152095f395f51905f526080610423838095614d96565b928181619ec0370191607f190160802080608052066169e05260a0610100619f409301925b838310614a165784835f5160206152095f395f51905f5284607f19016080208060805206616a005260a090619fc0916102008201925b8383106149f85784835f5160206152095f395f51905f5284607f19016080208060805206616a205260a090618500610cc08201905b8183106149c75750505f5160206152095f395f51905f528192607f19016080208060805206616a40525f5160206152095f395f51905f5260206080208060805206616a6052610120608061050683614d00565b928181616ac0370191607f190160802092836080526001600160801b035f5160206152095f395f51905f5260a0950616616a8052826182a052015b80821061499f57506080905f5160206152095f395f51905f52611ec493607f1901832080845206616aa05261057581614d00565b508181616b4037010361007457616a205180915f5b6014811061498457506127805192616cc0846127c0515b617060831061495f57505050506105da5f5160206152095f395f51905f525f5160206152695f395f51905f528408918261706052614e2a565b925f5160206152095f395f51905f52616cc092612760519009916127c0515b617060821061493a578585616ce05190616d00915b616e00831061491d575f92611ee4905b61214482106148f857505061706051616cc05190616e005193616cc052616ce052616d0052616d2052616d4052616d6052801561007457616a0051610300525f61a300525f5b61014081106148e957506001805b603182106148c05782618b4051618a40516185205190618a6051916185405161088052618a8051936185605194618aa0516108c0526185805190618ac0516185a0516108a052618ae05191886185c0519586946108805189618b0051905f5160206152095f395f51905f529109905f5160206152095f395f51905f529109955f5160206152095f395f51905f529109936108a0515f5160206152095f395f51905f52910992866108c051905f5160206152095f395f51905f529109925f5160206152095f395f51905f52910990610880515f5160206152095f395f51905f52908c09905f5160206152095f395f51905f528a8c095f5160206152095f395f51905f529108905f5160206152095f395f51905f529108905f5160206152095f395f51905f529108905f5160206152095f395f51905f529108905f5160206152095f395f51905f529108905f5160206152095f395f51905f529108905f5160206152095f395f51905f5291088684618b2051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5291085f5160206152095f395f51905f5290600109956103005161a30051905f5160206152095f395f51905f5291099661a1c051905f5160206152095f395f51905f52910861a1c0526108a0515f5160206152095f395f51905f52035f5160206152095f395f51905f52905f08915f5160206152095f395f51905f52035f5160206152095f395f51905f52905f089061088051905f5160206152095f395f51905f529108905f5160206152095f395f51905f529108905f5160206152095f395f51905f5291085f5160206152095f395f51905f529060010961a1e051905f5160206152095f395f51905f52910861a1e0525f5160206152095f395f51905f52035f5160206152095f395f51905f52905f08915f5160206152095f395f51905f529108905f5160206152095f395f51905f5291085f5160206152095f395f51905f529060010961a20051905f5160206152095f395f51905f529108906185e0515f5160206152095f395f51905f52035f5160206152095f395f51905f52905f089061088051905f5160206152095f395f51905f529108905f5160206152095f395f51905f5291085f5160206152095f395f51905f5290600109918261a9605261030051906103005190610300515f5160206152095f395f51905f529109905f5160206152095f395f51905f529109905f5160206152095f395f51905f52910961a3005261a360515f5160206152095f395f51905f529109905f5160206152095f395f51905f52910861a20052614120905f9061a960825b84906152ef861015611f0357600186515f1a9601959182600514611ed7575081600614611eb75781600814611e9c5781600d14611e725781601014611e4e5781601114611e2a5781602114611b8b5750806019146118405780601f146112ea5780601b14610b2b57600b14610aba575f80fd5b600384519401935f905f5160206152095f395f51905f526103005161a300510961a300525f5160206152095f395f51905f5285611fe08360f31c1661a1c0019283519061ffff8160e81c16610b13575b50089052610a47565b90621fffe0849260e31c1661a3400151900989610b0a565b505082516002909301925f925061a96090839060f01c8015610e575780600114610d2b5780600214610c4657600314610b62575f80fd5b5f5160206152095f395f51905f528080808080618b0051816185e05181035f0890088180618520518161858051918009097f404d21073985d14e432a4ad76d3fae06ca74314b950fe7b1d7f501cd31a8b374099008818061854051816185a051918009097f0b2cc8704264c6bd81bc620e9e524d4b73e9b2317679422ff7fa1603955649f10990088180618560518161862051918009097f0fdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f099008600109808452816103005161a300510961a300528161a2e05161a3605190090861a2e052610a47565b505f5160206152095f395f51905f528080808080618ae051816185c05181035f0890088180618520518161858051918009097f1b8114c381b922fd5d6d241210e2d8a68ad5744053ba9e776118de4107b51ace099008818061854051816185a051918009097f3df32e4cc4cb2ed20e5d21899cf5331775990ccaec4c09b4e3717213fcc0d7630990088180618560518161862051918009097f3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc1099008600109808452816103005161a300510961a300528161a2e05161a3605190090861a2e052610a47565b505f5160206152095f395f51905f528080807f73eda753299d7d483339d80809a1d7f7b67900f7fe6bfad98998021b7bb5732b81807f73eda753299d7d483339d80809a1d80553bca402fffe5bfeffffffff0000000181808080600160701b61852051088180600160701b6185405108600160381b0990088180600160701b6185605108600160701b099008818080618660518161868051600160381b0990086186a0518290600160701b09900881035f08900808818070241eabfffeb153ffffb9feffffffffaaab6187c0510981035f089008088180600160861b817f73eda753299d7d483339d80809a1d80553bda402fffe5b6e855000003ab000026187e051080981035f089008600109808452816103005161a300510961a3005261a240510861a24052610a47565b5061852051610400526185c051610520526185e051610480526186005161044052618540516104a0526187a0516104c0525f5160206152095f395f51905f528080807f73eda753299d7d483339d80809a1d7d340dd972492de594de627fffefcb803498180618560516187805161054052618580516105805261876051610460526185a0516104205261874051610500526186205161056052618640516104e05281808080618660518161868051600160381b0990086186a0518290600160701b09900881035f089181808061044051600160701b09818061048051600160381b096105205108089181808083600160701b0981806104a051600160381b09610400510808918180806104c0516104e05109700c8557e86f90d0d89eed6eb5349a0f88200991818080610540516104e05109702cb9b546d20373eaf85e8f53db883cb5480991818080610460516104e0510970013af65741744bd7bb2c6872df2b8003200991818080610500516104e0510970340f2ebe380a0f5eff4360543988a61dc20991818080610440516104e0510970297784894e27525bc342b7fde37dba93660991818080610480516104e05109703212e00cde6d2002b119d800000347fcb809918180806104c0516105605109702cb9b546d20373eaf85e8f53db883cb548099181808061054051610560510970013af65741744bd7bb2c6872df2b800320099181808061046051610560510970340f2ebe380a0f5eff4360543988a61dc2099181808061050051610560510970297784894e27525bc342b7fde37dba93660991818080610440516105605109703212e00cde6d2002b119d800000347fcb809918180806104c051610420510970013af65741744bd7bb2c6872df2b800320099181808061054051610420510970340f2ebe380a0f5eff4360543988a61dc2099181808061046051610420510970297784894e27525bc342b7fde37dba93660991818080610500516104205109703212e00cde6d2002b119d800000347fcb809918180806104c051610580510970340f2ebe380a0f5eff4360543988a61dc2099181808061054051610580510970297784894e27525bc342b7fde37dba93660991818080610460516105805109703212e00cde6d2002b119d800000347fcb809918180806104c051840970297784894e27525bc342b7fde37dba936609918180808080610540518609703212e00cde6d2002b119d800000347fcb80993610520519009600160701b098180806104c0516104a05109703212e00cde6d2002b119d800000347fcb809818080610480516104a05109600160701b09818080610520516104a05109600160381b09818080610440516104005109600160701b09818080610480516104005109600160381b09816105205161040051090808080808080808080808080808080808080808080808080808080808818070241eabfffeb153ffffb9feffffffffaaab6187c0510981035f089008088180600160861b817f73eda753299d7d483339d80809a1d80553b9202d7ffe85d4800008bb200000016187e051080981035f089008600109808452816103005161a300510961a3005261a220510861a22052610a47565b505090505f905f61a96090616d40516103a052616d005161032052616d20516103e0526169a05161038052616980516103c0525f5160206152095f395f51905f52806190e05181610320516103a0510809816103005161a30051090861a300525f5160206152095f395f51905f52618b6051816103c05191816103c0515f0908095f5b6004811061181357505060015f5b600481106117f15750600161a9e05260015b600481106117bf5750600161aac0526003805b61178d57505f905f5b600481106117605750905f5160206152095f395f51905f52808080808096816190c05197810391880908816103005161a3005109089381808080618be051948180618b8051816103c0515f090881618ba051916103c05190090895096190e0510881036191005108816190a0519361038051900890090881806103e05161032051088103600108099161030051900908610360526191605161034052618a00516102e05261852051610260526185405161028052618560516102c052618580516102a0526185a0516102405261862051610220526186405161020052618660516101e052618680516101c0526186a0516101a0526186c051610180526186e0516101605261870051610140526187205161012052618bc051610100526191405160e0525f5160206152095f395f51905f5280618c005181035f0860010860c0525f5160206152095f395f51905f52808060e05160010961034051088103619180510860a0525f5160206152095f395f51905f5280806191205181806103805181806101005160c05109816103c05181806101205160c05109816103c05181806101405160c05109816103c05181806101605160c05109816103c05181806101805160c05109816103c05181806101a05160c05109816103c05181806101c05160c05109816103c05181806101e05160c05109816103c05181806102005160c05109816103c05181806102205160c05109816103c05181806102405160c05109816103c05181806102a05160c05109816103c05181806102c05160c05109816103c05181806102805160c05109816103c05181806102605160c05109816103c05181806102e05160c05109816103c0515f09080908090809080908090809080908090809080908090809080908090809080860a051090881806103e0516103205108810360010809816103005181805f5160206152695f395f51905f528180610380518161010051816103c0518161012051816103c0518161014051816103c0518161016051816103c0518161018051816103c051816101a051816103c051816101c051816103c051816101e051816103c0518161020051816103c0518161022051816103c0518161024051816103c051816102a051816103c051816102c051816103c0518161028051816103c0518161026051816103c051816102e051816103c0515f09080908090809080908090809080908090809080908090809080908090809080860e0510908816103005181806103405181610320516103a051080981610300516103605109080908090861a30052610a47565b915f5160206152095f395f51905f52600191818560051b8061aa6001519061a9e0015109900892016113a9565b5f5160206152095f395f51905f528160051b808601519061aa600151095f19820160051b61aa6001525f1901806113a0565b6001905f5160206152095f395f51905f525f19820160051b808701519061a9e00151098160051b61a9e001520161138d565b905f5160206152095f395f51905f526001918360051b8601519009910161137b565b8060019160051b5f5160206152095f395f51905f526103805181836185400151870808908601520161136d565b5050618a205161a9609081525f925082805b60058110611b7257506185005161aa2052616d605161aa40525f5b60098110611b595750618a005161ab80525f5b60128110611b4057505f5b60068110611b2557505f5b60068110611b0a57505f5b60058110611aef57505f5160206152095f395f51905f5280808061ade0518103600108616d405109816103005161a30051090881808061ae80518181810391800908616d005109916103005190090861a3005260015b60068110611aa957505f5160206152095f395f51905f52616a20516169a0510961b000525f5b6006811061192b5750610a47565b600381026003810160128111611aa1575b8260051b908161aea001519161ade001519061b00051908194906169c051906169a0515b8a8285106119d457505050505050600193925f5160206152095f395f51905f52808080957f4285088329c399ea457a8ca1d30f8957e74c7f529842a1579b4fee55b398292395820390088180616d2051616d0051088103890809816103005161a30051090861a300520961b000520161191d565b9285979385969792939482809760051b809301519261aba001515f5160206152095f395f51905f529087095f5160206152095f395f51905f52908408905f5160206152095f395f51905f5291085f5160206152095f395f51905f529109985f5160206152095f395f51905f529108905f5160206152095f395f51905f5291085f5160206152095f395f51905f529109947f08634d0aa021aaf843cab354fabb0062f6502437c6a09c006c083479590189d75f5160206152095f395f51905f52910993600101929190611960565b50601261193c565b805f5160206152095f395f51905f52808060019460051b61ade001515f19850160051b61af60015182039008616d405109816103005161a30051090861a30052016118f7565b80606060019202618ec001518160051b61af600152016118a1565b80606060019202618ea001518160051b61aea0015201611896565b80606060019202618e8001518160051b61ade001520161188b565b8060019160051b80618c4001519061aba0015201611880565b8060019160051b8061862001519061aa6001520161186d565b8060019160051b8061852001519061a980015201611852565b92939480915090600181515f1a91015f9260018316611e1a575b505f9360028316611e01575b815196875f1a8860011a908960021a9a60058b60041a960199611df3575b505f5b818110611da65750505f5b818110611d475750505f5b878a821015611c7f5788519860118a60f01c9101995f5b60078110611c135750505050600101611be8565b8060051b8301515f6080525b600760805110611c325750600101611bff565b9a5f5160206152095f395f51905f5290818d81600460805187018a0101515f1a60051b612ae001519160805160051b61ffff8960e01c16015190090990089a600160805101608052611c1f565b5050959750955f905b8060031a8210611d0d5750505f905b808210611cc9575050600116611cb1575b50916001610a47565b905f5160206152095f395f51905f5291510984611ca8565b90935f5160206152095f395f51905f526001918160058b519b019a81815f1a60051b612ae001519161ffff808260d81c16519160e81c165109099008940190611c97565b90945f5160206152095f395f51905f526001918160038c519c019b61ffff8160e81c1651905f1a60051b612ae00151099008950190611c88565b6002895160f01c990198515f905b8a60078310611d6957505050600101611bdd565b600191929a5f5160206152095f395f51905f5260038193519e019d8161ffff825f1a60051b612ae001519260e81c16518709099008990190611d55565b5f5b8a60078210611dbb575050600101611bd2565b6001919a5f5160206152095f395f51905f5260038193519e019d61ffff8160e81c1651905f1a60051b612ae001510990089901611da8565b84526020909301928b611bcf565b9350600181515f1a91019060051b612ae0015193611bb1565b905160f01c925060030187611ba5565b935f5160206152095f395f51905f5291506002865160f01c96019551900992610a47565b935f5160206152095f395f51905f5291506002865160f01c96019551900892610a47565b935f5160206152095f395f51905f529150600186515f1a96019560051b612ae00151900992610a47565b935f5160206152095f395f51905f52809250035f0892610a47565b93915f5160206152095f395f51905f529150601f19019182510892610a47565b92949150946003905160f01c920194611ef5575b5051916001610a47565b835260209092019184611eeb565b83906152ef870361007457610074576169e051610840525f5160206152095f395f51905f52618ae051815f5160206152695f395f51905f526185c051099008610720526185205161082052618540516108005261856051610760525f5160206152095f395f51905f526107605161076051096107e05261858051610860525f5160206152095f395f51905f52610860516108605109610740526185a0516107a0525f5160206152095f395f51905f526107a0516107a051096107805261862051610700525f5160206152095f395f51905f526107005161070051096107c052618640516106e0525f5160206152095f395f51905f526106e0516106e051096106c052618660516106a0525f5160206152095f395f51905f526106a0516106a05109610680525f5160206152095f395f51905f52618b0051815f5160206152695f395f51905f526185e05109900861066052618b205161064052618b405161062052618a405161060052618a60516105e052618a80516105c0525f5160206152095f395f51905f52618aa051815f5160206152695f395f51905f52618600510990086105a0525f5160206152095f395f51905f5280808080618c205181036001086191a0519009810381808080806106805161068051096106a051095f5160206152295f395f51905f5209818080806106c0516106c051096106e051095f5160206152895f395f51905f5209818080806107c0516107c0510961070051095f5160206151e95f395f51905f5209818080806107805161078051096107a051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e098180808061074051610740510961086051097ed94c46a8456352aa44d7a885ab59e3a36664e6fb25e826f8a4cd79822f053309818080806107e0516107e0510961076051097f1f61345b652161410c5e29f51e301ae56342af824bc110649393d2b911c50d3e098180610800517f40fa389feb2522bb934881ac9ed749aee2296502af592418c6b5675c0f560261098180610820517f70d8f2a733a64d650faccc9b1c2a766a9544bb3ff1a11ee73cb43947ef386633096105a0510808080808080808816108405181808080806106c0516106c051096106e051095f5160206152295f395f51905f5209818080806107c0516107c0510961070051095f5160206152895f395f51905f5209818080806107805161078051096107a051095f5160206151e95f395f51905f52098180808061074051610740510961086051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e09818080806107e0516107e0510961076051097ed94c46a8456352aa44d7a885ab59e3a36664e6fb25e826f8a4cd79822f0533098180610800517f27e7119226c42a6d19c1541904b99ae40685511ed2e078964b74594d38340849098180610820517f6d05a41959f539a7fc9ec0972ea1e3dbb6fc67dd51daf3414f7fbbb091c7274a0981805f5160206152695f395f51905f526106a051096105c0510808080808080808816108405181808080806107c0516107c0510961070051095f5160206152295f395f51905f5209818080806107805161078051096107a051095f5160206152895f395f51905f52098180808061074051610740510961086051095f5160206151e95f395f51905f5209818080806107e0516107e0510961076051097f1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e098180610800517f23a6684b942d726a22e4d5b8d8ff83aeaa773f62600184efe5d033d7c7c6e827098180610820517f2f5908b169c6cf1bd26dcf0f9e5105481f5164f3ece0582bf3098312167751a70981805f5160206152695f395f51905f526106e051096105e05108080808080808816108405181808080806107805161078051096107a051095f5160206152295f395f51905f52098180808061074051610740510961086051095f5160206152895f395f51905f5209818080806107e0516107e0510961076051095f5160206151e95f395f51905f52098180610800517f24822e1af9aa2887c912c87eb0f20bd332330e7e55cd784de67cb407a9f05520098180610820517f726df1506749848155630b86ae25a82b281ecd050fe3a52d85a181fa87202e4b0981805f5160206152695f395f51905f526107005109610600510808080808088161084051818080808061074051610740510961086051095f5160206152295f395f51905f5209818080806107e0516107e0510961076051095f5160206152895f395f51905f52098180610800517f26c2cc87f95726b28f33ca03409a460ec987cfe12adae32769e3565865d07191098180610820517f222e83e70453dfee19b402e9fa8dfe2c4987b034d0be3ceb478b3022e97934c10981805f5160206152695f395f51905f526107a05109610620510808080808816108405181808080806107e0516107e0510961076051095f5160206152295f395f51905f52098180610800517f6bd72f9cfc53af9d931896e77ea5c61244cb6d5fae8954f37dc7b9002f5aa78a098180610820517f5e1d3dbecda6214343e24a47f45c5d033197ad01b65a730af95dc57e90c491400981805f5160206152695f395f51905f5261086051096106405108080808816108405181808080806106805161068051096106a051097f0fdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f09818080806106c0516106c051096106e051097f301cf56f9b4577112cc4241cddf6484aaadedbf1bbd0f2351adf2e41c2fb2ecd09818080806107c0516107c0510961070051097f5f3a15bab4ce4097b1edc3a25002694b92395ce355a8a12fe557459d9633f70109818080806107805161078051096107a051097f275a20361ea91992193920270d3e2d1f6361880ac0a439c64bef815d4469ba85098180808061074051610740510961086051097f31e823a45e567484c1544e310c0fa5cd66547a8f0dde659ac61698c30e838d2509818080806107e0516107e0510961076051097f26cc223e16f47c20e17cc6069605fa5a8af05ea4f6eb36029a641d23b818eb10098180610800517f4d0ea7f9c3fda06d9535b0fdafd8338bd47c2200b284fa71a325ff41ac358028098180610820517f5b1fc262a28cbb8bf75d9b1a6edaa74591ec24cd9a209512213cec3a3c0f1a5d09610660510808080808080808816108405181808080806106805161068051096106a051097f3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc109818080806106c0516106c051096106e051097f06ccb1c7d87f3c12a2bde4e68ac7f1e8b03481ba15d7f88f9a7f9b8310dd6d3409818080806107c0516107c0510961070051097f53fded36d490ba6b05a5d10fd99ffe5456baec6a6a8753199d5ebdc33c99790e09818080806107805161078051096107a051097f412c98232b6ab8a47aa76ee814ef7ec6261987c9802f2cfc490e007951a60ca5098180808061074051610740510961086051097f333f8046ece5579cbd6872449c57f2703dfc8864cfadc06d587ff104a0d0c1f209818080806107e0516107e0510961076051097f3509dd2fe3aac0080783557fec090fb1cb4b2b0901253c55282024331d1fe1a8098180610800517f52f789e4afc3801f7411102ee2f47cc5954a744e71cac98e75ea962a55a0a76f098180610820517f590ba402032e82eb1f660ef09796c5686345a5054ed96dae8e2d2336337887710961072051080808080808080881610840515f0908090809080908090809080908090808816103005161a3005109088161a9405161a1c0510961a1c0528161a9205161a1e0510961a1e0528161a8c05161a200510961a200528161a8605161a220510961a220528161a8005161a240510961a240528161a7a05161a260510961a260528161a7405161a280510961a280528161a6e05161a2a0510961a2a0528161a6805161a2c0510961a2c0528161a5c05161a2e0510961a2e05281035f08616d8052616a2051908160015f5b6014811061489d57505f5160206152095f395f51905f5280808080888180988198616da0528103600108616dc05281808061278051816127a051809c81809c81809c818c819d9b829c9a839b6170405282096170605209806170205209090909090909090961700052616a4051600161738052600190617380905f915b602a83106148715784618a0061a3005261850061a320526190a061a340526190c061a3605261912061a3805261914061a3a0526191a061a3c052618a2061a3e052618a4061a40052618a6061a42052618a8061a44052618aa061a46052618ac061a48052618ae061a4a052618b0061a4c052618b2061a4e052618b4061a50052618b6061a52052618b8061a54052618ba061a56052618bc061a58052618be061a5a052618c0061a5c052618c2061a5e052618c4061a60052618c6061a62052618c8061a64052618ca061a66052618cc061a68052618ce061a6a052618d0061a6c052618d2061a6e052618d4061a70052618d6061a72052618d8061a74052618da061a76052618dc061a78052618de061a7a052618e0061a7c052618e2061a7e052618e4061a80052618e6061a82052616d8061a84052618a00516173a061a32060015b602b811061484657505050617ba0526186e0515f5160206152095f395f51905f526189a0519181806173a051928184618700510990089381836189c05109900882806173c051958187618720510990089181866189e05109900890617bc052617be052818080619060518180619080519281886190e051099008956191005109900892818661916051099008936191805109900890617c0052617c205261852061a300526185c061a3205261884061a3405261854061a360526185e061a3805261886061a3a05261856061a3c05261860061a3e05261888061a4005261858061a4205261874061a440526188a061a460526185a061a4805261876061a4a0526188c061a4c05261862061a4e05261878061a500526188e061a5205261864061a540526187a061a5605261890061a5805261866061a5a0526187c061a5c05261892061a5e05261868061a600526187e061a6205261894061a640526186a061a6605261880061a6805261896061a6a0526186c061a6c05261882061a6e05261898061a70052618520516185c05161884051916173a061a36060015b600b81106147fd57505050617c4052617c6052617c8052618e8061a30052618ea061a32052618ec061a34052618ee061a36052618f0061a38052618f2061a3a052618f4061a3c052618f6061a3e052618f8061a40052618fa061a42052618fc061a44052618fe061a4605261900061a4805261902061a4a05261904061a4c052618e8051618ea051618ec051916173a061a36060015b600581106147b457505050617ca052617cc052617ce052616a6051616a80516182a0516170005191836170205181617040519581617060519188815f5160206152095f395f51905f52035f5160206152095f395f51905f529089085f5160206152095f395f51905f528581038a085f5160206152095f395f51905f528381038b08905f5160206152095f395f51905f529109905f5160206152095f395f51905f529109915f5160206152095f395f51905f5281810383085f5160206152095f395f51905f5286810384085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52908409895f5160206152095f395f51905f5283810388085f5160206152095f395f51905f5285810389085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5290830994878d5f5160206152095f395f51905f5282810387085f5160206152095f395f51905f5288810388085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5290890961303990614c8d565b955f5160206152095f395f51905f5283810382085f5160206152095f395f51905f5289810383085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52908809935f5160206152095f395f51905f5282810385085f5160206152095f395f51905f528a810386085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f529086095f5160206152095f395f51905f528381038b085f5160206152095f395f51905f528681038c085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52908209995f5160206152095f395f51905f5286810389085f5160206152095f395f51905f528281038a08905f5160206152095f395f51905f5291095f5160206152095f395f51905f52908c099a8b945f5160206152095f395f51905f52035f5160206152095f395f51905f52908a085f5160206152095f395f51905f529109905f5160206152095f395f51905f52035f5160206152095f395f51905f529089085f5160206152095f395f51905f529082099788965f5160206152095f395f51905f52035f5160206152095f395f51905f5291085f5160206152095f395f51905f529109915f5160206152095f395f51905f52910981617ca051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5203935f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52916080013509905f5160206152095f395f51905f529108925f5160206152095f395f51905f52910990617cc051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52035f5160206152095f395f51905f529108925f5160206152095f395f51905f52910990617ce051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52035f5160206152095f395f51905f5291085f5160206152095f395f51905f52825f09905f5160206152095f395f51905f52910887895f5160206152095f395f51905f5285810388085f5160206152095f395f51905f5282810389085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f528881038b085f5160206152095f395f51905f528781038c085f5160206152095f395f51905f528481038d08905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291098a5f5160206152095f395f51905f528a810385085f5160206152095f395f51905f5289810386085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5290830991885f5160206152095f395f51905f528c810382085f5160206152095f395f51905f5287810383085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52908509968c5f5160206152095f395f51905f52878a0961350090614c8d565b965f5160206152095f395f51905f52908809935f5160206152095f395f51905f5282810385085f5160206152095f395f51905f528a810386085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f529086095f5160206152095f395f51905f528381038b085f5160206152095f395f51905f528681038c085f5160206152095f395f51905f5290600109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52908209995f5160206152095f395f51905f5286810389085f5160206152095f395f51905f528281038a08905f5160206152095f395f51905f5291095f5160206152095f395f51905f52908c099a8b945f5160206152095f395f51905f52035f5160206152095f395f51905f52908a085f5160206152095f395f51905f529109905f5160206152095f395f51905f52035f5160206152095f395f51905f529089085f5160206152095f395f51905f529082099788965f5160206152095f395f51905f52035f5160206152095f395f51905f5291085f5160206152095f395f51905f529109915f5160206152095f395f51905f52910981617c4051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5203935f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52916060013509905f5160206152095f395f51905f529108925f5160206152095f395f51905f52910990617c6051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52035f5160206152095f395f51905f529108925f5160206152095f395f51905f52910990617c8051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52035f5160206152095f395f51905f529108915f5160206152095f395f51905f529109905f5160206152095f395f51905f529108905f5160206152095f395f51905f5281810389085f5160206152095f395f51905f52906001095f5160206152095f395f51905f5282810388085f5160206152095f395f51905f528a81038908905f5160206152095f395f51905f529109905f5160206152095f395f51905f529109905f5160206152095f395f51905f5289810382085f5160206152095f395f51905f52906001095f5160206152095f395f51905f529083096138b390614c8d565b5f5160206152095f395f51905f528a810383085f5160206152095f395f51905f52906001095f5160206152095f395f51905f52908209915f5160206152095f395f51905f528181038c085f5160206152095f395f51905f52906001095f5160206152095f395f51905f52908409905f5160206152095f395f51905f528c81038b085f5160206152095f395f51905f529083099384925f5160206152095f395f51905f528381038d085f5160206152095f395f51905f529109915f5160206152095f395f51905f52035f5160206152095f395f51905f52908c085f5160206152095f395f51905f528e81038d08905f5160206152095f395f51905f5291095f5160206152095f395f51905f52910981617c0051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5203915f5160206152095f395f51905f5291095f5160206152095f395f51905f529060408c013509905f5160206152095f395f51905f529108925f5160206152095f395f51905f52910990617c2051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52035f5160206152095f395f51905f529108915f5160206152095f395f51905f529109905f5160206152095f395f51905f529108905f5160206152095f395f51905f5281810387085f5160206152095f395f51905f52906001095f5160206152095f395f51905f5282810386085f5160206152095f395f51905f528881038708905f5160206152095f395f51905f5291095f5160206152095f395f51905f52828209925f5160206152095f395f51905f5289810382085f5160206152095f395f51905f52906001095f5160206152095f395f51905f52908509613b5490614c8d565b915f5160206152095f395f51905f528a810383085f5160206152095f395f51905f52906001095f5160206152095f395f51905f52908409935f5160206152095f395f51905f52908509935f5160206152095f395f51905f528b81038a085f5160206152095f395f51905f529086099485935f5160206152095f395f51905f52035f5160206152095f395f51905f52908b085f5160206152095f395f51905f529109915f5160206152095f395f51905f52910981617bc051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f5203915f5160206152095f395f51905f5291095f5160206152095f395f51905f529060208a013509905f5160206152095f395f51905f529108925f5160206152095f395f51905f52910990617be051905f5160206152095f395f51905f529109905f5160206152095f395f51905f5291095f5160206152095f395f51905f52035f5160206152095f395f51905f529108915f5160206152095f395f51905f529109905f5160206152095f395f51905f529108925f5160206152095f395f51905f52035f5160206152095f395f51905f529108613d1590614c8d565b90617ba0515f5160206152095f395f51905f52039035905f5160206152095f395f51905f529108905f5160206152095f395f51905f529109915f5160206152095f395f51905f529109905f5160206152095f395f51905f5291089081616e4052616aa05180808094616da051616dc051916182a051925f5160206152095f395f51905f52856001096001600160801b038116955f5160206152095f395f51905f5291096001600160801b038116965f5160206152095f395f51905f5291096001600160801b038116995f5160206152095f395f51905f5291096001600160801b038116975f5160206152095f395f51905f5291096001600160801b03169260806198c061a3005e600161a38052608061994061a3a05e6173c05161a420526080619d4061a4405e6173e05161a4c05260806199c061a4e05e6174005161a560526080619dc061a5805e6174205161a600526080619f4061a6205e6174405161a6a052608061578061a6c05e6174605161a74052608061550061a7605e6174805161a7e052608061558061a8005e6174a05161a88052608061560061a8a05e6174c05161a92052608061568061a9405e6174e05161a9c052608061570061a9e05e6175005161aa6052608061530061aa805e6175205161ab0052608061538061ab205e6175405161aba052608061540061abc05e6175605161ac4052608061548061ac605e6175805161ace052608061580061ad005e6175a05161ad8052608061588061ada05e6175c05161ae2052608061590061ae405e6175e05161aec052608061598061aee05e6176005161af60526080615b8061af805e6176205161b000526080615f0061b0205e6176405161b0a052608061600061b0c05e6176605161b14052608061608061b1605e6176805161b1e052608061610061b2005e6176a05161b28052608061618061b2a05e6176c05161b32052608061620061b3405e6176e05161b3c052608061628061b3e05e6177005161b46052608061630061b4805e6177205161b50052608061638061b5205e6177405161b5a052608061640061b5c05e6177605161b64052608061648061b6605e6177805161b6e052608061650061b7005e6177a05161b78052608061658061b7a05e6177c05161b82052608061660061b8405e6177e05161b8c052608061668061b8e05e6178005161b96052608061670061b9805e6178205161ba0052608061678061ba205e6178405161baa052608061680061bac05e6178605161bb4052608061688061bb605e6178805161bbe052608061690061bc005e6178a05161bc80526178c051915f5160206152095f395f51905f529083096080619fc061bca05e818161bd20525f5160206152095f395f51905f529109608061a04061bd405e818161bdc0525f5160206152095f395f51905f52910990608061a0c061bde05e8161be6052608061a14061be805e5f5160206152095f395f51905f52910961bf00526080615a0061bf205e61a1c0515f5160206152095f395f51905f5290820961bfa0526080615a8061bfc05e61a1e0515f5160206152095f395f51905f5290820961c040526080615b0061c0605e61a200515f5160206152095f395f51905f5290820961c0e0526080615c0061c1005e61a220515f5160206152095f395f51905f5290820961c180526080615c8061c1a05e61a240515f5160206152095f395f51905f5290820961c220526080615d0061c2405e61a260515f5160206152095f395f51905f5290820961c2c0526080615d8061c2e05e61a280515f5160206152095f395f51905f5290820961c360526080615e0061c3805e61a2a0515f5160206152095f395f51905f5290820961c400526080615e8061c4205e61a2c0515f5160206152095f395f51905f5290820961c4a0526080615f8061c4c05e61a2e0515f5160206152095f395f51905f52910961c54052608061974061c5605e8361c5e05260806197c061c6005e836173a051905f5160206152095f395f51905f52910961c68052608061984061c6a05e836173c051905f5160206152095f395f51905f52910961c720526080619cc061c7405e8461c7c0526080619e4061c7e05e846173a051905f5160206152095f395f51905f52910961c860526080619ec061c8805e846173c051905f5160206152095f395f51905f52910961c9005260806191c061c9205e8761c9a052608061924061c9c05e876173a051905f5160206152095f395f51905f52910961ca405260806192c061ca605e876173c051905f5160206152095f395f51905f52910961cae052608061934061cb005e876173e051905f5160206152095f395f51905f52910961cb805260806193c061cba05e8761740051905f5160206152095f395f51905f52910961cc2052608061944061cc405e8761742051905f5160206152095f395f51905f52910961ccc05260806194c061cce05e8761744051905f5160206152095f395f51905f52910961cd6052608061954061cd805e8761746051905f5160206152095f395f51905f52910961ce005260806195c061ce205e8761748051905f5160206152095f395f51905f52910961cea052608061964061cec05e876174a051905f5160206152095f395f51905f52910961cf405260806196c061cf605e876174c051905f5160206152095f395f51905f52910961cfe0526080619a4061d0005e8561d080526080619ac061d0a05e856173a051905f5160206152095f395f51905f52910961d120526080619b4061d1405e856173c051905f5160206152095f395f51905f52910961d1c0526080619bc061d1e05e856173e051905f5160206152095f395f51905f52910961d260526080619c4061d2805e8561740051905f5160206152095f395f51905f52910961d300526080616ac061d3205e868261d3a05261478d575b5f5160206152095f395f51905f5296979387809693948180808080809a8199099c60808601350999606085013509966040840135099360208301350990350808080808616e60526080616b40616f005e6080612860815e805f5160206152095f395f51905f52616e605181035f0861010052614778575b806080616e806101005e614762575b6080616b406101005e80616a80516101805261474b575b80614735575b608080616f805e7c70616972696e672d62617463682d6163632d6b7a670000000000000000610100526080616f806101205e6080616f006101a05e6080616c406102205e6080616bc06102a05e5f5160206152095f395f51905f52610220610100200690811561472c575b6080616c406101005e808261018052614715575b806080616f806101805e6146fd575b80916080616bc06101005e610180526146e6575b806080616f006101805e6146ce575b8015610074576146c290614f51565b50600160805260206080f35b506080616f0061010080600b5afa60803d14166146b3565b50608061010060a081600c5afa60803d14166146a4565b506080616f8061010080600b5afa60803d1416614690565b50608061010060a081600c5afa60803d1416614681565b6001915061466d565b5060808061010081600b5afa60803d1416614602565b50608061010060a081600c5afa60803d14166145fc565b5060808061010081600b5afa60803d14166145e5565b5060808060a081600c5afa60803d14166145d6565b9692955090925a616e806130c061a300600c608094fa3d608014169592969391909361455f565b909194606060205f5160206152095f395f51905f526001938180808c9b9a9b519b818d8c515109900899818c878c01515109900899604089015151099008970193019101612e7a565b909194606060205f5160206152095f395f51905f526001938180808c9b9a9b519b818d8c515109900899818c878c01515109900899604089015151099008970193019101612de4565b9091926020805f5160206152095f395f51905f52600193818851885151099008950193019101612c4a565b5f5160206152095f395f51905f52826020600193019509926001600160801b0384168552019192612b07565b91905f5160206152095f395f51905f528086818460019509099280099201612a8a565b5f5160206152095f395f51905f5260019161030051900991828160051b61a34001520190610672565b5f61a1c0820152602001610664565b909360205f5160206152095f395f51905f52819281883586510990089501910161061e565b5f5160206152095f395f51905f526020918451900892019161060e565b5f5160206152095f395f51905f528382828060209587510988098552099101906105f9565b602091815f5160206152095f395f51905f5280938103870885520991019085906105a1565b915f5160206152095f395f51905f528160019209920161058a565b9181355f5160206152095f395f51905f52811015610074578152602090810192910190610541565b90928235915f5160206152095f395f51905f52831015610074578281529181526020908101939281019291016104b3565b91906080614a07838293614d96565b9381848237019101919061047e565b91906080614a25838293614d96565b93818482370191019190610448565b9190926080614a44838293614d96565b938184823701910192909291926103ff565b916080614a67858293969496614d96565b9481848237019101919291926103db565b916080614a89858293969496614d96565b9481848237019101919291926103ca565b91906080614aa9838293614d96565b9381848237019101919061037b565b614ac6608092918392614d96565b92818582370192019190610343565b90602080918335945f5160206152095f395f51905f5286101694815201910161032b565b506080616c4060a061a1c0600c5afa60803d14166102ff565b9050614b28600160381b600760386120c4615030565b9392614b3e600160381b60076038612104615179565b5093919092169580614b82575b15614b5a575b505050506102ea565b909192948383178287171715151694616c4052616c6052616c8052616ca05283808080614b51565b95838317828617171516955f616c40525f616c60525f616c80525f616ca052614b4b565b915082915f616c40525f616c60525f616c80525f616ca0526102e4565b506080616bc060a061a1c0600c5afa60803d1416610261565b9050614bf2600160381b60076038612044615030565b9392614c08600160381b60076038612084615179565b5093919092169580614c4c575b15614c24575b5050505061024c565b909192948383178287171715151694616bc052616be052616c0052616c205283808080614c1b565b95838317828617171516955f616bc0525f616be0525f616c00525f616c2052614c15565b915082915f616bc0525f616be0525f616c00525f616c2052610246565b801561007457602061260052602061262052602061264052612660527f73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffffff612680525f5160206152095f395f51905f526126a052602061260060c08160055afa156100745760203d03610074576126005190565b80356040820135908060801c610074578160801c610074576001600160801b038091169116906f1a0111ea397fe69a4b1ba7b6434bacd75f5160206152495f395f51905f52602085013511158183141691101715610074576f1a0111ea397fe69a4b1ba7b6434bacd75f5160206152495f395f51905f526060840135111581831416911017156100745760809060a03761012090565b81356040830135908060801c610074578160801c610074576001600160801b038091169116906f1a0111ea397fe69a4b1ba7b6434bacd75f5160206152495f395f51905f52602086013511158183141691101715610074576f1a0111ea397fe69a4b1ba7b6434bacd75f5160206152495f395f51905f52606085013511158183141691101715610074576080809282370190565b8015614f4e575061a1c090616cc05191616ce0925b6170608410614f295783515f5160206152095f395f51905f5291098015614f2257602082526020808301526020604083015260608201527f73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffffff60808201525f5160206152095f395f51905f5260a082015260208160c08160055afa60203d141692815191601f1901905b80616ce010614efa5750505f5160206152095f395f51905f5280616ce051830991616cc051900990616cc052616ce052565b5f5160206152095f395f51905f5280835185099382519009928152601f199182019101614ec8565b505f925050565b60205f5160206152095f395f51905f5281928694965190099283865201930190614e3f565b90565b8015614f4e57506080616f806103005e6101006128e06103805e6080616f006104805e6101006129e06105005e60206103008080600f5afa60203d141661030051161561007457600190565b5f96945f969293945f1901925f5b868110614fbb5750505050505050565b8483820483808260051b880135921516615028575b5087858406021c16868202610100811080615007575b15614ff6575b5050600101614fab565b60ff19011b9099019860015f614fec565b9a82821b019a6101008983011115614fe6579b8282610100031c019b614fe6565b900383614fd0565b600193925f926003820160021c845b81811061513357505084833510156001166150ec575b93600491849582615067960294614f9d565b8192919381936f1a0111ea397fe69a4b1ba7b6434bacd781145f5160206152495f395f51905f5284148116806150e1575b156150d1575b6f1a0111ea397fe69a4b1ba7b6434bacd7905f5160206152495f395f51905f52600160801b891095111516911017161693565b600186019586109096019561509e565b5f9750879650615098565b936150679350815f5160206152495f395f51905f526f1a0111ea397fe69a4b1ba7b6434bacd76151228460048181988c8b614f9d565b929092149114169450915093615055565b828160021b8503600490818110615171575b5002610100811061515a575b5060010161503f565b9760018092991b8960051b87013510169790615151565b90505f615145565b9290915f926001946003830160021c5f5b8181106151a2575050925f9260049261506795614f9d565b838160021b86036004908181106151e0575b500261010081106151c9575b5060010161518a565b9760018092991b8960051b850135101697906151c0565b90505f6151b456fe4e5280109d8f96b8bfb543a6b1af25fb56a9db616af85a90eedc558e3eb1ea2973eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff000000014997c5aa3a5fa07bcaf880a9054bef831effbd9cd58e46d9bb4fb88ef99de0db64774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff000000004382d0938a760120dd6cef8f3b90a0c38abae475e3d21e39365472b76d780272 diff --git a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md index 0a2c41b79..d52700a91 100644 --- a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md +++ b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md @@ -8,16 +8,16 @@ chain. | Contract | Address | | --- | --- | -| `Halo2VerifyingKey` | [`0x80b32f68A333dF55Da60BEc44C498477a8311eDe`](https://sepolia.etherscan.io/address/0x80b32f68A333dF55Da60BEc44C498477a8311eDe) | -| `Halo2Verifier` | [`0x5CfEd44D16F994fc17f681A83FEdFEB9c3348c17`](https://sepolia.etherscan.io/address/0x5CfEd44D16F994fc17f681A83FEdFEB9c3348c17) | +| `Halo2VerifyingKey` | [`0x8C70F334Be6ba4Bd13Bb0a4F7A88FA640A9a0910`](https://sepolia.etherscan.io/address/0x8C70F334Be6ba4Bd13Bb0a4F7A88FA640A9a0910) | +| `Halo2Verifier` | [`0x0AF93fb982cfBe7f8Cc12E58123497Ba68dCb021`](https://sepolia.etherscan.io/address/0x0AF93fb982cfBe7f8Cc12E58123497Ba68dCb021) | ## Transactions | Action | Transaction | | --- | --- | -| Deploy VK | [`0xf96f25bd4f44b815a93b5d3a8f3c59d8f62afd4d6a10adf60139c73c5b7243dc`](https://sepolia.etherscan.io/tx/0xf96f25bd4f44b815a93b5d3a8f3c59d8f62afd4d6a10adf60139c73c5b7243dc) | -| Deploy verifier | [`0x5b34c4f25cdd6a641150488b0dc5edb130d0e8fd160408789f8ecd09f99bf417`](https://sepolia.etherscan.io/tx/0x5b34c4f25cdd6a641150488b0dc5edb130d0e8fd160408789f8ecd09f99bf417) | -| Verify fresh proof | [`0x013fb1a6323d51574c0c5b1c2e79ec4fb84706474c1c06601fa6e61737eb15a4`](https://sepolia.etherscan.io/tx/0x013fb1a6323d51574c0c5b1c2e79ec4fb84706474c1c06601fa6e61737eb15a4) | +| Deploy VK | [`0x8be21d8a313620dd040128f88ef01aac7e5c4e44e1f2ff2302f10c9cf28869a9`](https://sepolia.etherscan.io/tx/0x8be21d8a313620dd040128f88ef01aac7e5c4e44e1f2ff2302f10c9cf28869a9) | +| Deploy verifier | [`0xb3cf7ce86434d640a72a0286c8af7fb3974b69073ea3a9356a31368de4a461fb`](https://sepolia.etherscan.io/tx/0xb3cf7ce86434d640a72a0286c8af7fb3974b69073ea3a9356a31368de4a461fb) | +| Verify fresh proof | [`0x993e2357241a4ee87330e66fbd940c900255528cb66eb05634287a46cc27761c`](https://sepolia.etherscan.io/tx/0x993e2357241a4ee87330e66fbd940c900255528cb66eb05634287a46cc27761c) | ## Gas @@ -25,7 +25,7 @@ chain. | --- | ---: | | Deploy VK | `3,723,458` | | Deploy verifier | `5,295,315` | -| Verify fresh proof | `1,291,730` | +| Verify fresh proof | `1,291,766` | The verifier was rendered without gas checkpoints. The proof verification transaction emitted zero logs. @@ -35,7 +35,7 @@ transaction emitted zero logs. | Contract | Runtime bytes | Runtime code hash | | --- | ---: | --- | | `Halo2VerifyingKey` | `17025` | `0x24430c63ec2017f12ec4004d66c66314f418dd77fe38dd4c2475c8f2b259e009` | -| `Halo2Verifier` | `21161` | `0x2e1e46a83888a8989698b1f3ad87387db52408e015b11d7cff24a930b0baa776` | +| `Halo2Verifier` | `21161` | `0xa85c3e8df15a46a685e405f57cfc85993228550db803d34a001b73d6c55bf61a` | ## Files diff --git a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/deployment.json b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/deployment.json index 0015e53f4..ae40c94fd 100644 --- a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/deployment.json +++ b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/deployment.json @@ -4,8 +4,8 @@ "deployer": "0x609A4Ff8347Bb82d65a12A597298F256Fd4DE493", "contracts": { "Halo2VerifyingKey": { - "address": "0x80b32f68A333dF55Da60BEc44C498477a8311eDe", - "deploymentTx": "0xf96f25bd4f44b815a93b5d3a8f3c59d8f62afd4d6a10adf60139c73c5b7243dc", + "address": "0x8C70F334Be6ba4Bd13Bb0a4F7A88FA640A9a0910", + "deploymentTx": "0x8be21d8a313620dd040128f88ef01aac7e5c4e44e1f2ff2302f10c9cf28869a9", "gasUsed": 3723458, "runtimeBytes": 17025, "runtimeCodeHash": "0x24430c63ec2017f12ec4004d66c66314f418dd77fe38dd4c2475c8f2b259e009", @@ -13,18 +13,18 @@ "sourceFile": "Halo2VerifyingKey.sol" }, "Halo2Verifier": { - "address": "0x5CfEd44D16F994fc17f681A83FEdFEB9c3348c17", - "deploymentTx": "0x5b34c4f25cdd6a641150488b0dc5edb130d0e8fd160408789f8ecd09f99bf417", + "address": "0x0AF93fb982cfBe7f8Cc12E58123497Ba68dCb021", + "deploymentTx": "0xb3cf7ce86434d640a72a0286c8af7fb3974b69073ea3a9356a31368de4a461fb", "gasUsed": 5295315, "runtimeBytes": 21161, - "runtimeCodeHash": "0x2e1e46a83888a8989698b1f3ad87387db52408e015b11d7cff24a930b0baa776", + "runtimeCodeHash": "0xa85c3e8df15a46a685e405f57cfc85993228550db803d34a001b73d6c55bf61a", "runtimeBytecodeFile": "Halo2Verifier.runtime.bytecode.hex", "sourceFile": "Halo2Verifier.sol" } }, "proofVerificationTx": { - "transactionHash": "0x013fb1a6323d51574c0c5b1c2e79ec4fb84706474c1c06601fa6e61737eb15a4", - "gasUsed": 1291730, + "transactionHash": "0x993e2357241a4ee87330e66fbd940c900255528cb66eb05634287a46cc27761c", + "gasUsed": 1291766, "status": 1 }, "diagnostics": { From f53abeac9f80eca692fb82ec8e0ce8d789f0bf8e Mon Sep 17 00:00:00 2001 From: Julien Coolen Date: Tue, 19 May 2026 13:18:57 +0100 Subject: [PATCH 19/19] Fix clippy proof layout iteration --- proofs/solidity-verifier/src/test.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/proofs/solidity-verifier/src/test.rs b/proofs/solidity-verifier/src/test.rs index 206b6a359..b3d738002 100644 --- a/proofs/solidity-verifier/src/test.rs +++ b/proofs/solidity-verifier/src/test.rs @@ -2862,14 +2862,16 @@ fn proof_reader_sections_cover_exactly_once( section.name, section.start, section.end, layout.proof_cptr, layout.proof_end )); } - for byte in section.start - layout.proof_cptr..section.end - layout.proof_cptr { - if let Some(previous_idx) = owners[byte] { + let section_start = section.start - layout.proof_cptr; + let section_end = section.end - layout.proof_cptr; + for (byte, owner) in owners.iter_mut().enumerate().take(section_end).skip(section_start) { + if let Some(previous_idx) = *owner { return Err(format!( "{} byte {byte:#x} overlaps {}", section.name, sections[previous_idx].name )); } - owners[byte] = Some(section_idx); + *owner = Some(section_idx); } } if let Some(byte) = owners.iter().position(Option::is_none) {