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:
- Note maturity - notes mature 10 blocks after creation. Proving at
currentBlock - 10guarantees every unspent note in your registry has matured at the proof base. - 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_blocksold (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.