Kaspalytics Learn is a work in progress.
Updated: Aug 2, 2026

ZK Scripts

Locking Kaspa UTXOs behind a RISC Zero zero-knowledge proof - the ZK script builder, Groth16 vs succinct receipts, and the on-chain spend.

Kaspa can verify a RISC Zero zero-knowledge proof natively on chain, via the OpZkPrecompile script opcode. The SDKs’ ZK script builder turns a RISC Zero receipt — the proof artifact — into the two halves of a P2SH lock: a redeem script that unlocks only when the node verifies a proof for a fixed program, and a sig script carrying the proof that 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 is 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 happens on the node.

For the conceptual background, see Zero Knowledge in Learn.

Four RISC Zero terms

TermWhat it isSize
image idA digest of the guest program’s compiled binary — which program ran.32 bytes
journalThe program’s public output — what it chose to reveal.varies
journal hashA digest of the journal — pins what the program output.32 bytes
receiptThe 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 function id (currently only poseidon2).

For anything going on chain, use Groth16. A succinct receipt will usually blow past the transaction mass limit.

Locking

Commit the builder to a program by its image id, take the redeem script, wrap it in a P2SH lock, and turn that into a fundable address:

use kaspa_txscript_zk_sdk::zk_to_script::ZkScriptBuilder;
use kaspa_txscript::pay_to_script_hash_script;

// From your prover: the guest program's 32-byte image id.
let image_id: [u8; 32] = hex::decode(IMAGE_ID)?.try_into().unwrap();

let builder = ZkScriptBuilder::new_r0_with_flags(flags).commit_to_groth16(image_id)?;

let redeem = builder.script();
let spk = pay_to_script_hash_script(redeem);
const { ZkScriptBuilder, payToScriptHashScript, addressFromScriptPublicKey } = kaspa;

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

const builder = ZkScriptBuilder.newR0({ flags: { covenantsEnabled: true } });
builder.commitToGroth16(IMAGE_ID);

const redeemScript = builder.script();
const lockingScript = payToScriptHashScript(redeemScript);
const p2shAddress = addressFromScriptPublicKey(lockingScript, 'testnet-10');
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)

spk = pay_to_script_hash_script(builder.script())
addr = address_from_script_public_key(spk, "testnet")

Send funds to that address and they’re locked behind the proof.

Redeeming

To spend, finalize the builder with the receipt and the journal hash. Finalizing returns a FinalizedR0Script holding both halves — the proof-bearing sig script for the input, and the redeem script it unlocks:

const finalized = builder.finalizeWithGroth16Proof(SERIALIZED_RECEIPT, JOURNAL_HASH);

// Use the sig script as the input's signature script, then submit as normal.
transaction.inputs[0].signatureScript = finalized.sigScript;
finalized = builder.finalize_with_groth16_proof(SERIALIZED_RECEIPT, JOURNAL_HASH)

# Use finalized.sig_script as the input's signature script, then submit as normal.

Both halves are hex strings. redeemScript / redeem_script is the same script you hashed at lock time — handy if you finalize in a different process than the one that built the address, since you can derive the P2SH script-public-key straight from it rather than rebuilding the committed builder.

In Rust, the equivalent is finalize_with_proof(receipt, journal_hash) on the committed builder, which takes a typed Groth16Receipt rather than serialized bytes. The fixed-journal variant bakes the journal hash into the redeem script, so its finalize_with_proof(receipt) takes the receipt alone.

The builder is a state machine: committing to a program returns a differently-typed builder that can be finalized, and calling a method in the wrong state is an error. In Rust that’s enforced at compile time by the type parameter; in Python and JavaScript it raises at runtime.

Everything else is ordinary Kaspa work — building the funding transaction, building the spend, submitting both. See Transactions.

Composing by hand

Below the staged builder there’s a set of fragment methods (append_* / push_*) plus standalone decoders that turn a receipt into the pieces the script needs. Reach for these when you’re embedding a verifier inside a larger script — a covenant that requires both a proof and a signature, say — rather than using the proof as the sole spending condition.

There are also fixed-journal variants: commit to a specific journal at lock time, so the script verifies not just “this program ran” but “this program ran and output exactly this”.

Full examples

  • examples/zk/groth16_onchain.py — the whole lifecycle end to end on testnet-10, using a vendored Groth16 proof set: build the scripts, lock a funding UTXO into the P2SH address, then spend it back by presenting the proof, verified on chain by OpZkPrecompile.
  • The WASM SDK release zip ships zkproof/groth16.js, groth16_builder.js, groth16_split.js and succinct equivalents.

Where to next

Kaspalytics strives to provide accurate data - our highest level of effort is given to data validation and maintenance. However, we cannot guarantee 100% accuracy. Data is subject to inaccuracies and change.

Contact | © 2026 Kaspalytics