Zero-Knowledge Proofs
Panda integrates zero-knowledge proof systems to enable verifiable computation, private inference, and rollup settlement. The @proof decorator marks contract methods that require proof generation, and the prover infrastructure handles the rest.
The @proof Decorator
Mark any @call or @query method with @proof() to require ZK proof generation for that method's execution. The decorator is defined in panda-sdk/panda/contract.py and accepts two parameters: type and backend.
Several production contracts in the Panda repo use @proof today. Here is how the FederatedTrainer contract applies it to gradient updates, ensuring every training contribution is verifiably correct:
from panda import contract, constructor, call, query, event, proof
@contract
class FederatedTrainer:
"""On-chain federated linear model with validity proofs on gradient updates."""
class State:
weights: list = []
bias: float = 0.0
round: int = 0
contributions: int = 0
contributors: list = []
owner: str = ""
input_dim: int = 0
output_dim: int = 1
total_samples_seen: int = 0
biases: list = []
@constructor
def deploy(self, ctx, input_dim=1, output_dim=1):
"""Deploy the federated model with specified dimensions."""
if input_dim < 1 or input_dim > 10000:
raise ValueError(f"input_dim must be between 1 and 10000, got {input_dim}")
if output_dim < 1 or output_dim > 10000:
raise ValueError(f"output_dim must be between 1 and 10000, got {output_dim}")
self.state.owner = ctx.sender
self.state.input_dim = input_dim
self.state.output_dim = output_dim
self.state.weights = [0.0] * (input_dim * output_dim)
self.state.biases = [0.0] * output_dim
self.emit(
event.ModelInitialized(
input_dim=input_dim,
output_dim=output_dim,
owner=ctx.sender,
)
)
@call
@proof(type="validity")
def contribute_gradient(self, ctx, x, y, learning_rate=0.01):
"""Submit a gradient update computed on private local data.
The contract computes the gradient of MSE loss for the provided
data batch and applies a single SGD step. This way, raw data
leaves the contributor's machine, but in a production deployment
the contributor would compute gradients locally and submit only
the gradient tensors. Here we compute on-chain for simplicity
and to demonstrate the full execution trace for ZK proofs.
Args:
x: list of input samples. Each sample is a list of floats
(length = input_dim), or a single float for 1D input.
y: list of target values. Each target is a float for single-output,
or a list of floats for multi-output.
learning_rate: SGD learning rate (default 0.01)
"""
# ... gradient computation and SGD weight update ...
@query
def predict(self, x) -> list:
"""Run inference on the current global model."""
# ... forward pass through the linear model ...
@query
def model_info(self) -> dict:
"""Return current model state and training statistics."""
return {
"weights": self.state.weights,
"bias": self.state.bias,
"biases": self.state.biases,
"round": self.state.round,
"contributions": self.state.contributions,
"contributors": self.state.contributors,
"input_dim": self.state.input_dim,
"output_dim": self.state.output_dim,
"total_samples_seen": self.state.total_samples_seen,
}
And here is how the PrivateModel contract combines @proof with @query for verifiable encrypted inference:
@query
@proof(type="validity")
def predict_private(self, encrypted_input) -> str:
"""Run inference on encrypted input and return encrypted output.
Args:
encrypted_input: hex-encoded encrypted JSON string containing:
{
"x": [[x11, x12, ...], ...] -- input samples
}
Returns:
hex-encoded encrypted JSON string containing:
{
"predictions": [y1, y2, ...],
"model_version": int
}
"""
if not self.state.is_initialized:
raise RuntimeError("Model not initialized")
key_hash = self.state.key_hash
# Decrypt input
input_json = _decrypt_data(encrypted_input, key_hash)
input_data = json.loads(input_json)
x_data = input_data["x"]
input_dim = self.state.input_dim
# Normalize 1D input
if isinstance(x_data[0], (int, float)):
x_data = [[float(v)] for v in x_data]
# Decrypt model weights
weights = json.loads(_decrypt_data(self.state.encrypted_weights, key_hash))
bias = json.loads(_decrypt_data(self.state.encrypted_bias, key_hash))
# Run inference
predictions = []
for sample in x_data:
pred = bias
for i in range(input_dim):
pred += sample[i] * weights[i]
predictions.append(round(pred, 6))
# Encrypt output
output = {
"predictions": predictions,
"model_version": self.state.training_count,
}
encrypted_output = _encrypt_data(json.dumps(output), key_hash)
return encrypted_output
The ZKPMembership contract uses @proof on its authorize method to ensure Merkle proof verification is itself provably correct:
@call
@proof(type="validity")
def authorize(self, ctx, member_id: str, proof_data: list, nullifier: str):
"""Verify membership and record an anonymous authorization.
The caller proves they are a member of the whitelist by providing
a valid Merkle proof. The nullifier prevents the same member from
authorizing twice (it is sha256(member_id + secret) computed
off-chain).
Args:
member_id: the member's ID (leaf in the Merkle tree).
proof_data: Merkle proof -- list of {"hash": hex, "position": str}.
nullifier: sha256(member_id + secret) -- unique, unlinkable token.
"""
if not nullifier or len(nullifier) == 0:
raise ValueError("nullifier is required")
# Check nullifier has not been used
if nullifier in self.state.nullifiers:
raise ValueError("nullifier already used -- possible double authorization")
# Verify Merkle proof of membership
if not merkle_verify(member_id, proof_data, self.state.root):
raise ValueError("invalid membership proof")
# Record the nullifier
nullifiers = dict(self.state.nullifiers)
nullifiers[nullifier] = ctx.block_height
self.state.nullifiers = nullifiers
self.state.authorized_actions = self.state.authorized_actions + 1
self.emit(
event.MemberAuthorized(
nullifier=nullifier,
block=ctx.block_height,
total_authorizations=self.state.authorized_actions,
)
)
Proof Types
Validity Proofs
@proof(type="validity")
A validity proof (ZK-SNARK or ZK-STARK) proves that the computation was executed correctly. The proof is generated during execution and verified on-chain or by any third party.
Properties:
- Proof is generated at execution time
- Verification is fast and cheap (constant-time for SNARKs)
- The verifier learns the output is correct without re-executing
- Suitable for high-value computations where correctness must be guaranteed
Use cases:
- ML model training (prove the model was trained honestly)
- Financial computations (prove a swap/liquidation was fair)
- Cross-chain message verification (prove an L2 state transition is valid)
Fraud Proofs
@proof(type="fraud")
A fraud proof executes optimistically -- the computation runs without generating a proof upfront. During a challenge window, anyone can submit a fraud proof to demonstrate the execution was incorrect.
Properties:
- No upfront proof generation cost
- Challenge window (typically hours to days)
- If challenged, the disputed computation is re-executed in a verification environment
- Lower cost when execution is usually honest
Use cases:
- Rollup settlement (optimistic rollups)
- Low-stakes computations where re-execution is acceptable
- High-throughput scenarios where proof generation would be a bottleneck
Proof Backends
The backend parameter selects which ZK proof system to use:
@proof(type="validity", backend="auto") # Default: auto-select
@proof(type="validity", backend="risc_zero") # RISC Zero zkVM
@proof(type="validity", backend="halo2") # Halo2 IPA-based proofs
@proof(type="validity", backend="sp1") # SP1 (Succinct) prover
Backend Selection
| Backend | Best For | Proof Size | Proving Time |
|---|---|---|---|
auto | General use -- selects based on trace complexity | Varies | Varies |
risc_zero | General-purpose computation (any Python code) | ~200 KB | Moderate |
halo2 | Arithmetic circuits, ML inference | ~1 KB | Fast |
sp1 | Complex programs, large traces | ~100 KB | Fast |
auto (default) analyzes the execution trace and selects the most efficient backend:
- Simple arithmetic/linear algebra ->
halo2 - Complex control flow or library calls ->
risc_zero - Large programs with many steps ->
sp1
RISC Zero
RISC Zero compiles the Python execution into a RISC-V trace and generates a ZK proof of correct execution. This works with any Python code, including complex library calls.
Halo2
Halo2 uses IPA (Inner Product Argument) polynomial commitments for efficient proofs of arithmetic circuits. Best for ML inference and numerical computation.
SP1 (Succinct)
SP1 handles large programs efficiently with recursive proof composition.
How Proofs Work in the Execution Pipeline
1. Execution
When a @proof-decorated method is called, PandaVM executes it normally while recording an execution trace.
2. Trace Generation
The execution trace captures:
- All bytecode operations executed
- Memory reads and writes
- State changes
- Input/output values
3. Proof Generation
The prover (panda-prover) takes the execution trace and generates a ZK proof:
- For
validityproofs: proof is generated immediately - For
fraudproofs: proof is only generated if challenged
4. Verification
The proof can be verified:
- On-chain: By a verifier contract (Groth16 for SNARKs, FRI for STARKs)
- Off-chain: By any node or client with the verification key
- Cross-chain: By settlement contracts on Ethereum L1
Groth16 Verification
For rollup settlement, proofs are verified on Ethereum using a Groth16 verifier contract. This provides constant-time verification regardless of the computation size.
Execution Trace -> RISC Zero/Halo2/SP1 Proof -> Groth16 Wrapper -> On-Chain Verification
The Groth16 wrapper converts the native proof format into a format verifiable by an Ethereum smart contract with a single verify(proof, publicInputs) call.
Combining @proof with @private
The PrivateModel contract demonstrates this pattern directly -- it uses both @private on the class and @proof(type="validity") on the predict_private method:
@private
@contract
class PrivateModel:
"""Privacy-enabled linear regression model with encrypted state."""
class State:
encrypted_weights: str = ""
encrypted_bias: str = ""
encrypted_metadata: str = ""
owner: str = ""
key_hash: str = ""
is_initialized: bool = False
training_count: int = 0
input_dim: int = 0
# ... constructor and training methods ...
@query
@proof(type="validity")
def predict_private(self, encrypted_input) -> str:
"""Prediction is:
1. Private: model weights are encrypted in state
2. Verified: ZK proof confirms the prediction was computed correctly
"""
# ... decryption, inference, re-encryption ...
Combining @proof with @receiver
Cross-chain messages can require proof verification:
from panda import contract, call, receiver, proof, event
@contract
class VerifiedBridge:
class State:
total_verified: int = 0
@receiver(chains=["ethereum"])
@proof(type="validity")
def on_verified_deposit(self, ctx, amount: int, depositor: str):
"""Deposit processing is proven correct."""
self.state.total_verified += amount
self.emit(event.VerifiedDeposit(amount=amount, depositor=depositor))
Note: The
@receiver+@proofcombination shown above is a reference pattern. The@receiverdecorator is defined in the SDK but no production contracts currently combine it with@proof.
Testing @proof Contracts
In local testing with ContractTestRunner, @proof methods execute normally without generating actual proofs. The decorator is recognized but proof generation is skipped:
from panda.testing import ContractTestRunner
runner = ContractTestRunner()
# Deploy a federated trainer
addr = runner.deploy("contracts/privacy/federated_trainer.py",
sender="alice", input_dim=2, output_dim=1)
# @proof methods work normally in tests -- contribute_gradient has @proof(type="validity")
runner.call(addr, "contribute_gradient", sender="alice",
x=[[1.0, 2.0], [3.0, 4.0]], y=[3.0, 7.0], learning_rate=0.01)
result = runner.query(addr, "predict", x=[[2.0, 3.0]])
# result.return_value is a list of predictions
Next Steps
- Read the Privacy guide for
@privatecontracts and encrypted state - See the Rollup Architecture for how proofs are used in settlement
- Try the Contract Playground to experiment with
@proofmethods - Check the ML Contracts guide for provable ML patterns