Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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",
]
111 changes: 111 additions & 0 deletions aggregation/src/ivc/prover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,115 @@ 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. 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::<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)
}
}
70 changes: 70 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,64 @@ 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 dual_msm = {
let _outer_challenge_layout =
midnight_proofs::poly::kzg::scoped_truncated_challenges(false);
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)?;
dual_msm
};

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