Proving Configuration

The proving provider sends your signed invocation to a proving service, which executes it in a virtual Starknet environment and returns a STARK proof. Three small conventions around it account for most submission failures.

Snippets assume transfers, account and provider from Getting Started.

import { constants } from "starknet"
import { ProvingServiceProofProvider } from "@starkware-libs/starknet-privacy-sdk"

const provingProvider = new ProvingServiceProofProvider(
  process.env.PROVING_SERVICE_URL!,
  constants.StarknetChainId.SN_SEPOLIA,
)

provingBlockId - always currentBlock - 10

const provingBlockId = (await provider.getBlockNumber()) - 10
const result = await transfers.build()./* ... */.execute({ provingBlockId })

The proof is generated against the state at provingBlockId. Two reasons to back off from the head:

  1. Note maturity - notes mature 10 blocks after creation. Proving at currentBlock - 10 guarantees every unspent note in your registry has matured at the proof base.
  2. Reorg buffer - a proof based on the chain head can be invalidated by an L2 reorg before the transaction lands. The contract allows proofs up to proof_validity_blocks old (currently 450), so ten blocks back is a comfortable, still-fresh margin.

Omitting it works most of the time - with intermittent Note not mature failures and worse proving-service cache hits. Just always pass it. And when chaining transactions (approve then deposit), re-fetch it after each waitForTransaction.

proofDetails - conditional, never empty

const proofDetails = callAndProof.proof.proofFacts?.length
  ? { proofFacts: callAndProof.proof.proofFacts, proof: callAndProof.proof.data }
  : {}
const tx = await account.execute(callAndProof.call, { tip: 0n, ...proofDetails })

Some providers (the mock/no-validate ones used in development) return empty proof facts. Passing proofFacts: [] through to account.execute makes starknet.js serialize an invalid v3 transaction - the keys must be omitted entirely, hence the conditional spread. tip: 0n is mandatory for v3 transactions; forgetting it fails with the cryptic Cannot mix BigInt and other types.

Retry hygiene: invalidateProofNonceCache()

The proving provider caches the pool nonce. After any failed submission - a revert, INVALID_NONCE, Replacement transaction underpriced - the cache is stale, and retrying loops on proofs the chain keeps rejecting:

transfers.invalidateProofNonceCache()
// ...then rebuild and resubmit

Deposits are screened on every proving route

A custom or self-hosted proving backend can prove every pool action, but a deposit is only accepted with a screening signature: FPI screens the depositing address and signs the deposit, and the pool verifies that signature onchain. Self-hosting is not a route around screening.

Teams running their own prover typically shield through a privacy-enabled wallet (Ready or Xverse) and then transfer privately to the account their integration controls. If your production flow needs direct deposits, raise it in the Cairo CoreStars Telegram.

Common failures

Symptom Cause Fix
Note not mature provingBlockId not backed off Use currentBlock - 10
Cannot mix BigInt and other types Missing tip Add tip: 0n
Revert with INVALID_PROOF_FACTS Passed proofFacts: [] Conditional spread
INVALID_NONCE on retry Stale cached pool nonce invalidateProofNonceCache() first

This is the last page of the series - head back to Getting Started to revisit the wiring.