Skip to content

feat: OP_VERIFY_ZKP opcode (Groth16 + PLONK)#78

Open
Kukks wants to merge 2 commits into
masterfrom
feat/op-verify-zkp
Open

feat: OP_VERIFY_ZKP opcode (Groth16 + PLONK)#78
Kukks wants to merge 2 commits into
masterfrom
feat/op-verify-zkp

Conversation

@Kukks
Copy link
Copy Markdown
Contributor

@Kukks Kukks commented May 3, 2026

Summary

Implements OP_VERIFY_ZKP (0xf6, replacing the OP_UNKNOWN246 reserved slot), a polymorphic SNARK verification opcode dispatching to consensys/gnark-backed Groth16 and PLONK verifiers over BLS12-381. Tracks #77.

Stack contract

<vk> <publicInputs> <proof> <zkp_type:u8>  OP_VERIFY_ZKP

The verifying key is committed in the script body (not the witness), so the tapleaf hash binds the spend to a specific circuit. On success the opcode pushes 1; on any failure (unknown zkp_type, malformed inputs, soundness rejection) it returns a script error.

Type registry

zkp_type System VK size Proof size Verifier cost Setup
0x01 Groth16 (BLS12-381) ~500 B ~200 B 3 pairings per-circuit trusted
0x02 PLONK (BLS12-381 + KZG) ~1 KB ~500 B ~5 pairings universal updatable
0x03+ reserved

Wire format for proof, vk, and publicInputs follows gnark's standard binary marshaling (WriteTo / ReadFrom). Public inputs are gnark witness.Witness bytes for the public-only witness vector.

Layout

File Purpose
pkg/arkade/zkp/zkp.go Verifier interface, Type enum, dispatch registry
pkg/arkade/zkp/groth16.go Groth16 backend (gnark)
pkg/arkade/zkp/plonk.go PLONK backend (gnark)
pkg/arkade/zkp/zkp_test.go Verifier-level round-trip tests
pkg/arkade/op_verify_zkp.go Opcode handler
pkg/arkade/op_verify_zkp_test.go Handler-level round-trip tests
pkg/arkade/opcode.go Constant OP_VERIFY_ZKP = 0xf6 + opcodeArray entry
pkg/arkade/opcode_test.go Replaces invalidSpec(OP_UNKNOWN246) with a verifyZkpSpec() that exercises malformed-input rejection paths

Why polymorphic instead of one opcode per system

Standard Bitcoin practice would allocate distinct opcodes per system. For Arkade specifically, the introspector binary is upgraded in coordination with arkd, so extending the dispatch table is operationally feasible. One opcode covers a growing family of proof systems without further allocation pressure, and a script can accept multiple zkp_type values via runtime branching.

The dispatch overhead at verification is a single map lookup — negligible against the underlying pairing math.

Tests

All tests generate proofs in-test via gnark's prover (no precomputed fixtures), exercising the full prove → marshal → verify-via-opcode round trip on a trivial x*x == y circuit. Includes both honest-proof acceptance and tamper / wrong-input rejection.

PLONK tests use gnark/test/unsafekzg.NewSRS for the in-test KZG SRS — explicitly not suitable for production deployments, which must use a public ceremony's SRS (Aztec Ignition, Polygon Ignition, etc.).

$ go test ./pkg/arkade/...
ok  	github.com/ArkLabsHQ/introspector/pkg/arkade	0.466s
ok  	github.com/ArkLabsHQ/introspector/pkg/arkade/zkp	0.273s

Test plan

  • All existing pkg/arkade tests pass with the renamed opcode constant
  • OP_VERIFY_ZKP honest Groth16 proof verifies and pushes 1
  • OP_VERIFY_ZKP honest PLONK proof verifies and pushes 1
  • Tampered proof rejects (script error)
  • Wrong public input rejects (script error)
  • Unknown zkp_type rejects with ErrUnknownType wrap
  • Malformed zkp_type byte (length != 1) rejects with ErrInvalidStackOperation
  • Stack underflow rejects with ErrInvalidStackOperation
  • go vet ./... passes
  • go build ./... passes

Open follow-ups (out of scope for this PR)

  • Cost accounting / weight units for SNARK verification (DoS protection)
  • Public-inputs canonical encoding spec (currently gnark's binary witness format)
  • Structural VK validation (currently relies on gnark's parser)
  • Fuzz target for (zkp_type, proof, public_inputs, vk) triples
  • Reserve future zkp_type allocations (Halo 2, Plonky2, STARK, Binius) — exceed practical witness budgets today, kept as footnote in the issue's type registry

Kukks added 2 commits May 3, 2026 19:16
Adds OP_VERIFY_ZKP at 0xf6 (replacing the OP_UNKNOWN246 reserved slot)
with Groth16 and PLONK verifier backends over BLS12-381. Tracks issue #77.

Stack expectation (top to bottom):
  zkp_type:u8  proof  publicInputs  vk

The verifying key is committed in the script body, not the witness — a
tapleaf using OP_VERIFY_ZKP includes PUSH <vk_bytes> as a constant, so
the tapleaf hash binds the spend to a specific circuit.

Type registry:
  0x01 = Groth16 (BLS12-381)        — per-circuit trusted setup, ~200 B proof
  0x02 = PLONK (BLS12-381 + KZG)    — universal updatable setup, ~500 B proof

Wire format for proof / vk / public-inputs follows gnark's standard
binary marshaling (WriteTo / ReadFrom).

Implementation:
  pkg/arkade/zkp/                   — verifier interface + dispatch registry
  pkg/arkade/zkp/groth16.go         — Groth16 backend (consensys/gnark)
  pkg/arkade/zkp/plonk.go           — PLONK backend (consensys/gnark)
  pkg/arkade/zkp/zkp_test.go        — round-trip tests with real proofs
  pkg/arkade/op_verify_zkp.go       — opcode handler
  pkg/arkade/op_verify_zkp_test.go  — handler tests with real proofs

Tests generate proofs in-test via gnark's prover (no precomputed fixtures),
exercising the full prove → marshal → verify-via-opcode round trip on a
trivial x*x == y circuit. Tampered proofs and wrong-input proofs are
verified to reject.
Address two HIGH-severity CVEs flagged by Trivy:

- CVE-2025-57801 in gnark v0.13.0: signature malleability in EdDSA and
  ECDSA implementations. Fixed in v0.14.0.
- GHSA-fj2x-735w-74vq in gnark-crypto v0.18.0: unchecked memory
  allocation during vector deserialization. Fixed in v0.19.2.

Neither vulnerability is reachable via OP_VERIFY_ZKP's verification path
(we use Groth16 / PLONK over BLS12-381, not EdDSA / ECDSA, and gnark's
own bounded WriteTo / ReadFrom serializers, not raw vector deserializers).
But they're shipped in the binary, and the security scan blocks merge,
so the upgrade is the right move regardless of reachability.
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