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_idran and produced public output whose hash isjournal_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
| 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_ididentifying the recursion circuit, and a hash function id (currently onlyposeidon2).
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 byOpZkPrecompile.- The WASM SDK release zip ships
zkproof/groth16.js,groth16_builder.js,groth16_split.jsand succinct equivalents.
Where to next
- Zero Knowledge — what this makes possible.
- Covenant Opcodes —
OpZkPrecompilealongside the rest. - Transactions — funding and spending the P2SH address.