Prove and verify with Plonky3 STARKs
Create Plonky3 STARK proofs for the settlement and identity circuits, verify them on the network, and check the attested commitment from an EVM contract.
Tenzro Network 1 uses Plonky3 STARKs, which need no trusted setup and rest on hash functions rather than elliptic curves, so they are quantum-safe. The network uses them in three places: to compress the finality certificates of Tenzro DAG Consensus, and in two application circuits you can call yourself:
- settlement: proves that a payment is bound to a specific service commitment and amount, that its nonce follows the previous one, and that the payer's balance covers it, without revealing the balance or the service proof.
- identity: proves control of a key, commits to a set of capabilities, and shows that a reputation score clears a minimum, without revealing the key, the capabilities or the score.
In this tutorial you create a proof for each circuit, have the network verify it, and read the resulting commitment from a contract.
Prerequisites
- The
tenzroCLI (see CLI reference). - Your own node for proving. A witness contains private values, so create proofs on a node you run (
http://127.0.0.1:8545below) and send only the proof to the public endpoint.
export LOCAL=http://127.0.0.1:8545
export RPC=https://rpc.tenzro.xyz1. List the circuits
tenzro zk circuits --rpc $RPC --format jsonEach entry names the circuit, the proof system (plonky3), the field (koala-bear) and the hash (poseidon2). This tutorial uses settlement and identity.
2. Prove a settlement
The settlement witness holds the payer's balance, the service proof, the current and previous nonces and the amount. Each value is a field element given as a decimal integer.
tenzro zk prove --rpc $LOCAL --format json \
--circuit-id settlement \
--witness '{
"payer_balance": 500,
"service_proof": 918273645,
"nonce": 42,
"prev_nonce": 41,
"amount": 120
}' > settlement-proof.json
jq '{circuit_id, proof_size_bytes, public_inputs: (.public_inputs | length)}' settlement-proof.json{ "circuit_id": "settlement", "proof_size_bytes": ..., "public_inputs": ... }The public inputs are hash commitments to the service and the settlement plus the amount. The balance, the service proof and the nonces stay private.
3. Verify on the network
Send the proof and its public inputs to the public endpoint:
tenzro zk verify --rpc $RPC --format json \
--circuit-id settlement \
--proof "$(jq -r .proof settlement-proof.json)" \
--inputs "$(jq -c .public_inputs settlement-proof.json)"{
"valid": true,
"circuit_id": "settlement",
"status": "plonky3_verified",
"commitment_hex": "0x...",
"newly_attested": true,
"attestation_model": "quorum"
}The same call is available as the open JSON-RPC method tenzro_verifyZkProof with circuit_id, proof and public_inputs.
A valid proof yields a commitment: a hash of the circuit ID, the proof and the public inputs. The commitment is admitted to the on-chain registry only after a stake-weighted quorum of validators has each re-run the verifier and co-signed it, and it then stays open to challenge for a fraud window. Change any public input and you get a different commitment.
4. Check the attestation window
tenzro zk attestation --commitment <commitment_hex> --rpc $RPCThe record shows when the commitment was attested and when its fraud window closes. Anyone who can show the proof does not verify may file a fraud proof during the window:
tenzro zk file-fraud-proof --commitment <commitment_hex> --rpc $RPC5. Use the commitment from a contract
Plonky3 STARKs are too large to verify inside every EVM transaction, so contracts read the attested result instead. The ZK_VERIFY precompile takes a 32-byte commitment and returns 1 if the validator quorum has attested it and 0 otherwise. Tenzro precompiles run inside ordinary EVM transactions, so a settlement contract can gate a payout on a proof:
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.24;
contract ProofGatedPayout {
address public immutable zkVerify; // the ZK_VERIFY precompile address, see /docs/zk
constructor(address precompile) { zkVerify = precompile; }
function attested(bytes32 commitment) public view returns (bool) {
(bool ok, bytes memory out) = zkVerify.staticcall(abi.encodePacked(commitment));
return ok && out.length == 32 && uint256(bytes32(out)) == 1;
}
function release(bytes32 commitment, address payable to, uint256 amount) external {
require(attested(commitment), "proof not attested");
to.transfer(amount);
}
}6. Prove an identity claim
The identity circuit lets an agent show that it controls a key and meets a reputation threshold while keeping the key, its capability set and its exact score private. The key here is a field element held by the prover for this purpose; it is not a wallet or validator key.
tenzro zk prove --rpc $LOCAL --format json \
--circuit-id identity \
--witness '{
"private_key": 7340281,
"capabilities": 13,
"capability_blinding": 99120443,
"actual_reputation": 87,
"minimum_reputation": 70
}' > identity-proof.json
tenzro zk verify --rpc $RPC --format json \
--circuit-id identity \
--proof "$(jq -r .proof identity-proof.json)" \
--inputs "$(jq -c .public_inputs identity-proof.json)"The public inputs are a commitment to the public key, a blinded commitment to the capabilities and the minimum reputation. A relying party that already knows the key commitment and the capability commitment learns only that the agent clears the threshold.
What these proofs are not for
Inference results on Tenzro are verified with TEE attestation, not with zero-knowledge proofs. To check that a model ran where and how a provider claims, use attested inference; see TEE.
Next steps
- ZK reference: ZK.
- How finality certificates are compressed: Finality certificates.
- Where settlement proofs fit: Settlement.