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:

<!-- Source: contracts/privacy/federated_trainer.py -->
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:

<!-- Source: contracts/privacy/private_model.py -->
    @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:

<!-- Source: contracts/privacy/zkp_membership.py -->
    @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

BackendBest ForProof SizeProving Time
autoGeneral use -- selects based on trace complexityVariesVaries
risc_zeroGeneral-purpose computation (any Python code)~200 KBModerate
halo2Arithmetic circuits, ML inference~1 KBFast
sp1Complex programs, large traces~100 KBFast

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 validity proofs: proof is generated immediately
  • For fraud proofs: 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:

<!-- Source: contracts/privacy/private_model.py -->
@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 + @proof combination shown above is a reference pattern. The @receiver decorator 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