Skip to content

feat(zk-verifier): register a native Groth16 host function (register_only) - #163

Merged
nol4lej merged 1 commit into
mainfrom
feat/groth16-host-function
Oct 10, 2026
Merged

nol4lej merged 1 commit into
mainfrom
feat/groth16-host-function

Conversation

@nol4lej

@nol4lej nol4lej commented Oct 9, 2026

Copy link
Copy Markdown
Member

feat(zk-verifier): register a native Groth16 host function (register_only)

Node 0.4.0 registers bn254_groth16_verify, which runs Groth16 verification natively from a key in its prepared form (#162). No runtime calls it yet: it ships #[version(1, register_only)], the pattern sp-io uses to introduce host functions. Spec 19 turns it on once every node runs a release that has it.

Why two steps

A runtime that imports a host function the node lacks cannot be instantiated: that node stops importing at the upgrade block, for good. Reproduced on a dev node: runtime requires function imports which are not present on the host: 'env:ext_groth_16_host_interface_bn254_groth16_verify_version_1', stuck at the upgrade block. So:

Release Node Runtime
0.4.0 / spec 18 (this PR) registers the function does not import it; verification stays in Wasm
between releases 2/3 of the authors declare 0.4.0+ (lastAuthorVersion); Root sets setMinAuthorVersion(0.4.0); RPC and archive operators upgrade —
spec 19 unchanged (version 1 stays) drops register_only and calls it

What changes

orbinum-zk-verifier 3.2.0:

  • host_interface (feature groth16-native, in default): Groth16HostInterface::bn254_groth16_verify(prepared_vk, proof, inputs) -> bool.
    • Frozen at version 1: any observable change becomes #[version(2)], and version 1 is never edited or removed, because syncing nodes re-execute blocks that call it.
    • Its exported symbol is pinned by a test.
  • verify_prepared(prepared_vk, proof, inputs): the one verification both paths run (the runtime in Wasm today, the node behind the host function in spec 19), so their answers cannot differ.
    • Every argument's shape is checked before any curve arithmetic: proof is PROOF_BYTES, inputs are a multiple of 32 bytes and exactly the key's arity, the prepared key's layout fits. Anything malformed is false.
    • It never panics: natively, a panic would take the node down instead of trapping the runtime.
  • prepared_arity: a prepared key's input count, read from its layout without deserializing a point.

pallet-zk-verifier 0.17.1:

  • Verification always runs from the key's prepared form (a key without one is prepared on the fly), through verify_prepared.
  • verify_proof (raw inputs) refuses inputs that do not fill the key before the pairing, as statement verification already did. It used to leave the count to the verifier.

Node: template/node/src/service.rs registers the host function in all four HostFunctions variants, benchmarks included. Registering a function no runtime imports is harmless.

Guards

Test Fails when
the_runtime_does_not_import_the_groth16_host_function_yet (runtime) the spec-18 runtime imports it by mistake. Decompresses the release blob and checks a positive control, Poseidon's import, so the search cannot pass vacuously. Flip it in spec 19
the_node_registers_the_groth16_host_function (node) a node build stops registering it (normal and runtime-benchmarks variants)
the_registered_symbol_is_frozen (primitives) renaming the trait or function changes the symbol a spec-19 runtime will import

Testing

Unit and property:

  • verify_prepared matches an independent oracle (the raw key prepared from scratch) on a real proof: valid statement, wrong input, non-canonical input.
  • Never verifies:
    • 2,000 random proofs;
    • every single-bit flip of a valid proof (1,024), and A↔C swapped;
    • 500 random inputs;
    • ~400 randomly corrupted prepared keys (never a wrong input; the result matches the reference).
  • Every malformed argument is false, with no panic.
  • orbinum-zk-verifier: 92 + 10. pallet-zk-verifier: 158 (166 with runtime-benchmarks,skip-proof-verification). pallet-shielded-pool, pallet-evm-precompile-shielded-pool and orbinum-runtime pass. clippy -D warnings (node included), fmt, taplo, try-runtime, no_std and wasm32 builds clean.

Live dev nodes:

Scenario Result
Fresh chain, prepared-key tampering (garbage, 2^40 prefix, another circuit's key, deleted key, re-registration) and verify_proof with 1, 8 and 10 inputs 20/20, fail closed
Node without the host function upgraded to this runtime 24/24, 0 instantiation errors: spec 18 is safe for nodes that have not upgraded
Cross-tree, spend hardening, key rules 42/42, 78/78, 12/12
With the runtime calling it (temporary build, the spec-19 setup) a transfer validates in ~1.5 ms instead of ~4.6 ms; upgrade from the previous runtime 21/21

Benchmarks: both pallets regenerate without failures and the output compiles. verify_proof measures 2.36 ms here, unchanged: verification is still in Wasm in spec 18. With the runtime calling the host function it is 0.61 ms. weights.rs is untouched.

Upgrade notes

  • Nothing for Root in spec 18: the host function is inert until spec 19.
  • Before spec 19's setCode, every node must be on 0.4.0+. The steps are documented in RUNTIME_VERSIONS.md §4.

@nol4lej
nol4lej merged commit 5b06089 into main Oct 10, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant