Skip to content

ZK Scripts

Kaspa can verify a RISC Zero zero-knowledge proof natively on-chain, via the OpZkPrecompile script opcode. The SDK's ZkScriptBuilder turns a RISC Zero receipt (the proof artifact) into the two scripts of a P2SH lock: a redeem script that only unlocks when the node verifies a proof for a fixed program, and a sig script that carries the proof which satisfies it.

The statement the chain checks is exactly:

"Program image_id ran and produced public output whose hash is journal_hash."

Anyone holding a valid proof for that pair can spend the UTXO — the proof is the authorization, no signature involved. The SDK does not generate proofs; you bring a receipt from a RISC Zero prover, and the bindings transcribe it into script bytes. Verification itself happens on the node.

Four RISC Zero terms

Term What it is Size
image id A digest of the guest program's compiled binary — which program ran. 32 bytes
journal The program's public output — what it chose to reveal. varies
journal hash A digest of the journal — pins what the program output. 32 bytes
receipt The proof plus the journal; the artifact you verify. see below

Groth16 vs succinct receipts

RISC Zero hands you the proof in one of two forms, and the builder has a parallel path for each:

  • Groth16 receipt — the STARK proof compressed into a Groth16 SNARK. A few hundred bytes, constant-size, cheap to verify. This is the practical on-chain choice; it relies on RISC Zero's public trusted-setup ceremony.
  • Succinct receipt — the STARK proof directly. No trusted setup, but hundreds of kilobytes, and verifying it needs extra material (a control_id identifying the recursion circuit, and a hash_fn_id — currently only "poseidon2").

The surface

Errors — a call in the wrong builder state, a malformed id, an undecodable receipt — raise ZkError from kaspa.exceptions.

Everything else (wrapping the redeem script in a P2SH address, building and submitting the transactions) is ordinary kaspa work — see Transactions and Scripts.

A minimal lock

from kaspa import ZkScriptBuilder, address_from_script_public_key, pay_to_script_hash_script

# From your prover: the guest program's 32-byte image id (hex).
IMAGE_ID = "75641a540ee2ad9ee5902bcdcdb8b55c0bef4a28287309b858f97b1356c6c2e0"

builder = ZkScriptBuilder.new_r0(covenants_enabled=True)
builder.commit_to_groth16(IMAGE_ID)

# The redeem script, wrapped in a P2SH lock and turned into a fundable address.
spk = pay_to_script_hash_script(builder.script())
addr = address_from_script_public_key(spk, "testnet")

Send funds to addr and they're locked behind the proof. To spend them, you finalize the builder with a receipt — that's Locking & Redeeming.

A full, live example

examples/zk/groth16_onchain.py runs the whole lifecycle end-to-end on testnet-10: it builds the zk scripts from a vendored Groth16 proof set, locks a funding UTXO into the P2SH address (commit), then spends it back by presenting the proof (redeem) — each a real on-chain transaction, with the redeem verified by OpZkPrecompile.

Secrets handling

Snippets here use literal values for readability. Never handle production secrets this way — see Security.

Next

Page What it covers
Locking & Redeeming The builder state machine, commit_to_* / finalize_with_*, FinalizedR0Script, and the on-chain spend.
Fragments & Helpers The append_* / push_* fragment methods, fixed-journal verifiers, and the standalone prepare_* decoders.