Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3ee7403
feat(transcript): add Keccak challenge sampling
jtcoolen May 15, 2026
02a2f73
feat(ivc): add Keccak final verifier fixtures
jtcoolen May 15, 2026
6220680
feat(kzg): split fewer point set controls
jtcoolen May 15, 2026
db26827
test(solidity): add native differential trace hooks
jtcoolen May 15, 2026
cf89e5f
feat(solidity): add Midfall verifier workspace
jtcoolen May 15, 2026
258814d
Add Moonlight Sepolia verifier tooling
jtcoolen May 19, 2026
5955ee4
Patch blake2b_halo2 transcript inference
jtcoolen May 19, 2026
57082b6
Add Halo2 verifier audit notes
jtcoolen May 19, 2026
ff28868
Update Moonlight Sepolia production verifier
jtcoolen May 19, 2026
b53e3eb
Add MCOPY constructor smoke test
jtcoolen May 19, 2026
2b2bf49
Forward gas to EIP-2537 precompiles
jtcoolen May 19, 2026
1e21311
Fail fast on invalid public instances
jtcoolen May 19, 2026
e64f289
Cross-check VK header constants
jtcoolen May 19, 2026
33f78d2
Assert quotient VM termination
jtcoolen May 19, 2026
780af85
Update Moonlight Sepolia verifier deployment
jtcoolen May 19, 2026
4bab29c
Deploy Moonlight Sepolia verifier without gas checkpoints
jtcoolen May 19, 2026
201aab0
Fix clippy proof layout iteration
jtcoolen May 19, 2026
c18fde7
Add Solidity verifier shape isolation test artifacts
jiajieey May 21, 2026
7c95f39
Expand Solidity verifier shape negative tests
jiajieey May 21, 2026
6abffd2
Add IVC adversarial calldata checks
jiajieey May 21, 2026
5c188ac
Add RSA Solidity verifier E2E coverage
jiajieey May 22, 2026
7f44ed3
Add SHA preimage Solidity verifier E2E coverage
jiajieey May 22, 2026
10ad02c
Record solc compatibility smoke results
jiajieey May 22, 2026
8178242
Harden real-circuit Solidity calldata negatives
jiajieey May 22, 2026
51f912c
Add hybrid Merkle tree Solidity verifier coverage
jiajieey May 22, 2026
978b1ff
test: add verifier differential mutation checks
jtcoolen Jun 1, 2026
743e9cd
Add quotient evaluator regression tests
jtcoolen Jun 1, 2026
349c2f3
Gate split quotient proof tests on truncation mode
jtcoolen Jun 1, 2026
aa628e8
test(solidity-verifier): expand component audit coverage
jtcoolen Jun 1, 2026
36f8146
test: cover all public input slots in fixtures
jtcoolen Jun 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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" }

Expand Down Expand Up @@ -37,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]
Expand Down
1 change: 1 addition & 0 deletions aggregation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 16 additions & 1 deletion aggregation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -50,7 +51,21 @@ 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`,
# 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",
]
106 changes: 106 additions & 0 deletions aggregation/src/ivc/prover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,110 @@ impl<T: Ivc> IvcProver<T> {
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<F>` (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<Vec<u8>, 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::<S>("self_vk", vk);

// Off-circuit verification of the previous proof still uses Poseidon
// because prior steps were generated under PoseidonState<F>. The
// resulting `proof_acc` is what we accumulate and what the IVC
// circuit re-verifies in-circuit.
let proof_acc = if is_genesis {
Accumulator::<S>::trivial(&fixed_bases.keys().cloned().collect::<Vec<_>>())
} else {
let prev_pi = [
AssignedVk::<S>::as_public_input(vk),
T::format_public_input(&self.state),
AssignedAccumulator::<S>::as_public_input(&self.acc),
]
.concat();

let mut transcript =
CircuitTranscript::<PoseidonState<F>>::init_from_bytes(&self.proof);
let dual_msm = plonk::prepare::<
F,
KZGCommitmentScheme<E>,
CircuitTranscript<PoseidonState<F>>,
>(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::<IvcCircuit<T>, Keccak256>(
&self.params,
&self.pk,
&self.relation,
&instance,
witness,
OsRng,
)?;

self.state = next_state;
self.proof = proof.clone();
self.acc = next_acc;

Ok(proof)
}
}
64 changes: 64 additions & 0 deletions aggregation/src/ivc/verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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<T: Ivc>(
&self,
ctx: &T::Context,
instance: &IvcInstance<T>,
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::<S>("self_vk", self.vk.vk());

let pi =
IvcCircuit::<T>::format_instance(instance).map_err(|_| IvcError::InvalidInstance)?;

let mut transcript = CircuitTranscript::<Keccak256>::init_from_bytes(proof);
let dual_msm = plonk::prepare::<F, KZGCommitmentScheme<E>, CircuitTranscript<Keccak256>>(
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::<S>::accumulate(&[proof_acc, instance.acc.clone()]);
if !final_acc.check(&self.params_verifier, &fixed_bases) {
return Err(IvcError::InvalidProof);
};

Ok(())
}
}
Loading
Loading