CPI
Why CPI
The CLI is the human surface. You stand at a terminal, type veria verify, and read the result with your eyes. That is the right shape for a person who wants to check one thing once.
Cross-Program Invocation is the program surface. It is what a Solana program calls when it — not a human — needs a verified fact before it acts. A lending market that will not lend against an unverified risk score. A leaderboard that will not record a run it cannot prove was legitimate. A mint that will not issue an NFT unless an ML inference actually returned the gating class.
The two surfaces wrap the same single instruction. Off-chain a prover folds the work into one SP1 Groth16 proof. On-chain verify_proof discharges it and writes a ProofRecord PDA. CPI is how one program asks another program to run that check inside the same transaction — so the verification and the action it gates are atomic. Either both land or neither does. That atomicity is the whole reason CPI exists: composable verified compute, with no trusted gap between checked and acted on.
The shape
Add the verifier crate to your program's Cargo.toml with the cpi feature. That feature pulls in the generated CPI helpers and turns off the on-chain entrypoint, so you link the verifier's instruction builders without shipping a second copy of its program:
[dependencies]
veria-verifier = { version = "0.1", features = ["cpi"] }
anchor-lang = "0.31"
The verifier exposes a single verify_proof instruction. Its accounts — declared as VerifyProof — are the submitter (signer and rent payer), the global VerifierConfig PDA, the ProofRecord PDA being created at [b"proof", &proof_hash], and the System Program. Your program holds those accounts in its own Accounts struct and forwards them.
Building the CPI call
The generated module gives you a typed accounts struct and a typed instruction function. You build a CpiContext, hand it the four accounts, and pass the same arguments the CLI would:
use anchor_lang::prelude::*;
use veria_verifier::cpi::accounts::VerifyProof;
use veria_verifier::cpi::verify_proof;
use veria_verifier::program::VeriaVerifier;
pub fn gate<'info>(
ctx: Context<'_, '_, '_, 'info, Gate<'info>>,
proof_hash: [u8; 32],
proof_bytes: Vec<u8>,
public_inputs: Vec<u8>,
circuit_id: u8,
expected_vk_hash: [u8; 32],
) -> Result<()> {
let cpi_accounts = VerifyProof {
submitter: ctx.accounts.submitter.to_account_info(),
config: ctx.accounts.verifier_config.to_account_info(),
proof_record: ctx.accounts.proof_record.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
};
let cpi_ctx = CpiContext::new(
ctx.accounts.verifier_program.to_account_info(),
cpi_accounts,
);
// Verification runs inside this transaction. A failed proof aborts
// everything below — the gated action never executes.
verify_proof(cpi_ctx, proof_hash, proof_bytes, public_inputs, circuit_id, expected_vk_hash)?;
// Reaching here means the proof verified on-chain. Act on it.
Ok(())
}
The ? is the entire safety property. If the SP1 Groth16 verification fails, verify_proof returns an error, the ? propagates it, and the Sealevel runtime reverts the whole transaction — including whatever your program intended to do next. There is no window in which an unverified result is treated as verified.
A TypeScript caller building the same instruction off-chain uses the SDK's CPI helper (@veria/sdk ≥ 0.1.6, dual require/import, circuit IDs 0-indexed to match the on-chain verifier) to assemble accounts and arguments; the on-chain shape above is what those bytes resolve to.
Three reference dApps
Three minimal Anchor programs show the pattern end to end. Each is one circuit, one CPI, one gated action:
- verified-feeds — a Pyth-style oracle. A folded
aggregationproof over many raw feeds verifies before the median price is written, so consumers read a number with a receipt. packages/examples/verified-feeds - gated-leaderboard — anti-cheat scoring. A
sort/permutation proof certifies a run was scored honestly before the entry is admitted, so the board cannot be stuffed with fabricated results. packages/examples/gated-leaderboard - ml-gated-mint — verified inference. An
ml-inferenceproof must return the gating class before the NFT mints, so the artwork is provably the output of the model, not a claim about it. packages/examples/ml-gated-mint
What CPI does not do
CPI does not produce the proof. The prover does that off-chain, where there is room for the real computation; the folded proof arrives as bytes. CPI does not re-run the work either — the on-chain verifier checks one SNARK, not the billions of cycles behind it.
What CPI adds is the on-chain receipt and the action that depends on it, fused into one atomic transaction. It moves verification from something a human reads afterward to something a program requires beforehand. The check stops being advisory. It becomes a precondition the runtime enforces.
Few dots. Whole truth.
The CLI lets a person see the dots. CPI lets a program build on them — and refuse to build on anything else.