Skip to content
Merged
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
7 changes: 5 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions frame/zk-verifier/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ All notable changes to this pallet are documented here.

## [Unreleased]

## [0.17.1] - 2026-10-09

### Changed

- Verification always works from the key's prepared form: a key with none is
prepared on the fly, and its arity is read from the prepared layout.
The pairing runs through `orbinum_zk_verifier::verify_prepared`, the same
function the node's `bn254_groth16_verify` host function runs.
- `verify_proof` (raw inputs) refuses inputs that do not match the key's arity
before the pairing, as statement verification already did; it used to leave
the count to the verifier.

## [0.17.0] - 2026-10-09

### Changed
Expand Down
2 changes: 1 addition & 1 deletion frame/zk-verifier/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pallet-zk-verifier"
version = "0.17.0"
version = "0.17.1"
description = "Zero-Knowledge proof verification pallet for Orbinum"
authors = ["Orbinum Team"]
license = "GPL-3.0-or-later"
Expand Down
7 changes: 6 additions & 1 deletion frame/zk-verifier/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -839,11 +839,16 @@ fn verify_proof_happy_path_emits_proof_verified_event() {
TRANSFER_PUBLIC_INPUTS + MEMO_HASH_INPUTS,
);
activate(CircuitId::TRANSFER, 1);
let inputs: PublicInputs = (0..TRANSFER_PUBLIC_INPUTS + MEMO_HASH_INPUTS)
.map(|_| vec![0x02u8; 32].try_into().unwrap())
.collect::<Vec<_>>()
.try_into()
.unwrap();
assert_ok!(ZkVerifier::verify_proof(
signed().into(),
CircuitId::TRANSFER,
proof_bytes(),
one_public_input(),
inputs,
));
assert!(has_event(Event::ProofVerified {
circuit_id: CircuitId::TRANSFER,
Expand Down
110 changes: 70 additions & 40 deletions frame/zk-verifier/src/verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ use crate::{
};
use alloc::vec::Vec;
use orbinum_zk_verifier::{
Bn254, InputLayout, PreparedVerifyingKey, VerifyingKey, has_memo_layout, input_layout,
prepared_from_stored,
InputLayout, VerifyingKey, has_memo_layout, input_layout, prepared_arity,
};

/// Verify a proof of a circuit statement, encoded for the layout of the key it
Expand All @@ -29,21 +28,20 @@ pub fn verify_statement<T: Config>(
proof: &[u8],
encode: impl FnOnce(InputLayout) -> Option<Vec<[u8; 32]>>,
) -> Result<(bool, u32), sp_runtime::DispatchError> {
check::<T>(circuit_id, version, proof, |layout, arity| {
encode(layout).filter(|raw| raw.len() == arity)
})
check::<T>(circuit_id, version, proof, encode)
}

/// Verify a proof against caller-supplied inputs, taken as-is. Serves the
/// `verify_proof` extrinsic, whose caller encodes every input itself.
/// Verify a proof against caller-encoded inputs, taken as-is (the
/// `verify_proof` extrinsic). Inputs that do not fill the key are
/// `valid = false`, as for a statement.
pub fn verify_raw<T: Config>(
circuit_id: CircuitId,
version: Option<u32>,
proof: &[u8],
raw_inputs: Vec<[u8; 32]>,
) -> Result<(bool, u32), sp_runtime::DispatchError> {
frame_support::ensure!(!raw_inputs.is_empty(), Error::<T>::EmptyPublicInputs);
check::<T>(circuit_id, version, proof, |_, _| Some(raw_inputs))
check::<T>(circuit_id, version, proof, |_| Some(raw_inputs))
}

/// The layout a key of `arity` inputs gives `circuit_id`, if admitted.
Expand All @@ -57,32 +55,41 @@ pub(crate) fn admitted_layout(circuit_id: CircuitId, arity: usize) -> Option<Inp
input_layout(id, arity).filter(|layout| !(has_memo_layout(id) && *layout == InputLayout::Base))
}

/// Shared path: resolve and prepare the key, build the inputs for its layout,
/// verify, record. `inputs` returns `None` to fail the proof without verifying.
/// The path both entry points share:
///
/// 1. Resolve the version and load its key, in prepared form.
/// 2. Read the key's arity and the layout it gives the circuit, if admitted.
/// 3. Build the inputs for that layout (`inputs` returns `None` to fail).
/// 4. Verify, if the inputs fill the key exactly.
/// 5. Count the outcome.
///
/// Any step that fails makes the proof invalid; only an unknown circuit or
/// version is an error.
fn check<T: Config>(
circuit_id: CircuitId,
version: Option<u32>,
proof: &[u8],
inputs: impl FnOnce(InputLayout, usize) -> Option<Vec<[u8; 32]>>,
inputs: impl FnOnce(InputLayout) -> Option<Vec<[u8; 32]>>,
) -> Result<(bool, u32), sp_runtime::DispatchError> {
frame_support::ensure!(!proof.is_empty(), Error::<T>::EmptyProof);
let (key, resolved) = resolve_key::<T>(circuit_id, version)?;

// Registration validated the key; if a stored one ever did not load, the
// proof fails rather than verifying under guessed inputs.
// 1. A key without a prepared form (storage written outside `keys`) is
// prepared here; one that does not prepare fails the proof.
let prepared = match key {
StoredKey::Prepared(bytes) => prepared_from_stored(&bytes),
StoredKey::Raw(bytes) => VerifyingKey::new(bytes).prepare(),
StoredKey::Prepared(bytes) => Some(bytes),
StoredKey::Raw(bytes) => VerifyingKey::new(bytes).prepared_bytes().ok(),
};
let result = match prepared {
Ok(pvk) => {
let arity = pvk.vk.gamma_abc_g1.len().saturating_sub(1);
// 2–4.
let result = prepared.is_some_and(|prepared| {
prepared_arity(&prepared).is_some_and(|arity| {
admitted_layout(circuit_id, arity)
.and_then(|layout| inputs(layout, arity))
.is_some_and(|raw| do_verify(&pvk, proof, raw))
}
Err(_) => false,
};
.and_then(inputs)
.filter(|raw| raw.len() == arity)
.is_some_and(|raw| do_verify(&prepared, proof, &raw))
})
});
// 5.
record_stats::<T>(circuit_id, resolved, result);
Ok((result, resolved))
}
Expand Down Expand Up @@ -123,26 +130,23 @@ fn record_stats<T: Config>(circuit_id: CircuitId, version: u32, result: bool) {
});
}

/// The pairing check.
/// The pairing, under a key in prepared form: [`verify_prepared`], the same
/// function the node's `bn254_groth16_verify` host function runs.
///
/// Always `true` in **test** builds, whose keys deserialize but carry no real
/// proofs. Benchmarks run the full pairing, so weights include it.
///
/// Always `true` in **test** builds: unit tests use keys that deserialize but
/// no real proofs. Benchmarks run the full pairing so weights reflect its cost.
fn do_verify(pvk: &PreparedVerifyingKey<Bn254>, proof: &[u8], raw_inputs: Vec<[u8; 32]>) -> bool {
/// [`verify_prepared`]: orbinum_zk_verifier::verify_prepared
fn do_verify(prepared: &[u8], proof: &[u8], inputs: &[[u8; 32]]) -> bool {
#[cfg(test)]
{
let _ = (pvk, proof, raw_inputs);
let _ = (prepared, proof, inputs);
true
}

#[cfg(not(test))]
{
use orbinum_zk_verifier::{Groth16Verifier, Proof, PublicInputs};
Groth16Verifier::verify_with_prepared_vk(
pvk,
&PublicInputs::new(raw_inputs),
&Proof::new(proof.to_vec()),
)
.is_ok()
orbinum_zk_verifier::verify_prepared(prepared, proof, &inputs.concat())
}
}

Expand Down Expand Up @@ -334,20 +338,41 @@ mod tests {
});
}

/// Raw inputs are not encoded or counted here; the count check against the
/// key is ark-groth16's, which the test build stubs out.
/// Raw inputs are never encoded: any values pass as long as they fill the key.
#[test]
fn verify_raw_does_not_encode_inputs() {
new_test_ext().execute_with(|| {
insert_vk(CircuitId::TRANSFER, 1, KEY);
activate(CircuitId::TRANSFER, 1);
assert_eq!(
verify_raw::<Test>(CircuitId::TRANSFER, None, &proof(), vec![[0x02; 32]]),
verify_raw::<Test>(CircuitId::TRANSFER, None, &proof(), vec![[0x02; 32]; KEY]),
Ok((true, 1))
);
});
}

/// Raw inputs that do not fill the key fail before the pairing, as a
/// statement's do.
#[test]
fn verify_raw_refuses_inputs_that_do_not_fill_the_key() {
new_test_ext().execute_with(|| {
insert_vk(CircuitId::TRANSFER, 1, KEY);
activate(CircuitId::TRANSFER, 1);
for count in [1, KEY - 1, KEY + 1] {
assert_eq!(
verify_raw::<Test>(
CircuitId::TRANSFER,
None,
&proof(),
vec![[0x02; 32]; count]
),
Ok((false, 1)),
"{count} inputs"
);
}
});
}

// ── Version resolution ────────────────────────────────────────────────────

#[test]
Expand Down Expand Up @@ -424,7 +449,12 @@ mod tests {
new_test_ext().execute_with(|| {
store(CircuitId::TRANSFER, 2, KEY);
insert_key(CircuitId::TRANSFER, 2, vec![0x01; 100].try_into().unwrap());
let res = verify_raw::<Test>(CircuitId::TRANSFER, Some(2), &proof(), vec![[0x02; 32]]);
let res = verify_raw::<Test>(
CircuitId::TRANSFER,
Some(2),
&proof(),
vec![[0x02; 32]; KEY],
);
assert_eq!(res, Ok((true, 2)));
});
}
Expand Down Expand Up @@ -513,7 +543,7 @@ mod tests {
for cid in [CircuitId::TRANSFER, CircuitId::UNSHIELD] {
insert_vk(cid, 1, KEY);
}
let raw = || vec![[0x02; 32]];
let raw = || vec![[0x02; 32]; KEY];
verify_raw::<Test>(CircuitId::TRANSFER, Some(1), &proof(), raw()).unwrap();
verify_raw::<Test>(CircuitId::UNSHIELD, Some(1), &proof(), raw()).unwrap();
verify_raw::<Test>(CircuitId::UNSHIELD, Some(1), &proof(), raw()).unwrap();
Expand Down
20 changes: 20 additions & 0 deletions primitives/zk-verifier/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@ All notable changes to this crate are documented here.

## [Unreleased]

## [3.2.0] - 2026-10-09

### Added

- `host_interface` (feature `groth16-native`, in `default`): Groth16
verification over BN254 as a native host function, `bn254_groth16_verify`,
from a key in its prepared form; `verify_proof` measured 2.43 → 0.61 ms once a
runtime uses it. Every malformed argument is `false` (proof not
`PROOF_BYTES`, inputs not a multiple of 32 bytes or not the key's arity,
non-canonical input, a key whose layout does not fit) and it never panics.
- It ships `#[version(1, register_only)]`: nodes register it, no runtime can
call it yet. A later runtime drops `register_only` once every node runs a
release with it; version 1 itself is frozen.
- `verify_prepared(prepared_vk, proof, inputs)`: verification on raw bytes from a
prepared key, checking every argument's shape first (`false` on any
malformation, never a panic). The host function's body, and what the runtime
runs in Wasm meanwhile: both paths run the same code.
- `prepared_arity`: a prepared key's input count, read from its layout without
deserializing a point.

## [3.1.0] - 2026-10-09

### Added
Expand Down
13 changes: 11 additions & 2 deletions primitives/zk-verifier/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "orbinum-zk-verifier"
version = "3.1.0"
version = "3.2.0"
authors = ["Orbinum Network <contact@orbinum.net>"]
edition = "2021"
publish = false
Expand Down Expand Up @@ -34,6 +34,9 @@ orbinum-zk-core = { path = "../zk-core", default-features = false }
parity-scale-codec = { version = "3.7.5", default-features = false, features = ["derive"], optional = true }
scale-info = { version = "2.11", default-features = false, features = ["derive"], optional = true }

# Native host function (`groth16-native`)
sp-runtime-interface = { version = "37.0.0", default-features = false, optional = true }

# For parsing verification key from JSON (std only)
num-bigint = { version = "0.4", default-features = false, optional = true }

Expand All @@ -45,7 +48,7 @@ ark-snark = { version = "0.5.0", default-features = false }
ark-std = { version = "0.5.0", default-features = false, features = ["std"] }

[features]
default = ["std"]
default = ["std", "groth16-native"]

# Standard library support (enables JSON parsing, BigInt, etc.)
std = [
Expand All @@ -57,8 +60,14 @@ std = [
"ark-std/std",
"num-bigint",
"orbinum-zk-core/std",
"sp-runtime-interface?/std",
]

# Groth16 verification host function. `no_std`-compatible: generates the Wasm
# import and the native implementation. Ships `register_only` (see
# `src/host_interface.rs`).
groth16-native = ["sp-runtime-interface"]

# Substrate runtime integration (requires codec)
substrate = [
"parity-scale-codec",
Expand Down
Loading
Loading