A note can belong to any Solana address, and a program-derived address is an address. So a program can hold shielded value — a treasury, an escrow, a payroll — and pay out of it without showing whom, or how much.
The idea
An address-owned note is spent with claim, and claim asks for one thing:
that the owning address signed. A wallet signs with its key. A PDA signs
through its program's invoke_signed. The pool cannot tell the difference,
and does not try.
note.pk = Poseidon2(pda_hi, pda_lo) the PDA's 32 bytes, split in two
No key material enters the circuit, so there is nothing for a program to lack.
Funding a PDA
Pay it like any other address. From the SDK:
import { addressRecipient, buildTransfer } from "@zksvm/sdk";
const toTreasury = await addressRecipient(treasuryPda, operatorEncPk);
await buildTransfer({ /* … */ outputs: [{ amount, ...toTreasury }, change] }, PROGRAM_ID);
The second argument matters. A PDA has no private key, so it cannot decrypt a
note encrypted to itself. Pass an X25519 key that whoever operates the program
holds, and they will find the note by scanning. Without it, deliver the
opening (amount, blinding) to the operator yourself.
Spending by CPI
The proof is built off-chain, by whoever operates the program, exactly as for a wallet:
const { instruction } = await buildClaim(
{ payer, owner: treasuryPda, input: note, outputs, fee: 0n, tree, artifacts },
PROGRAM_ID,
);
instruction names the PDA as a signer, which no transaction can satisfy
directly. Route it through your program instead: pass the same accounts and
data, and have the program forward them with its seeds.
let ix = Instruction {
program_id: *pool_program.key,
data: data.to_vec(),
accounts: forwarded.iter().map(|a| AccountMeta {
pubkey: *a.key,
is_signer: a.is_signer || *a.key == treasury,
is_writable: a.is_writable,
}).collect(),
};
invoke_signed(&ix, forwarded, &[&[b"treasury", &[bump]]])?;
That is the whole of programs/tests/treasury, the test fixture the on-chain
suite uses. A real treasury decides whether to sign — a vote passed, a
schedule came due — and that policy is ordinary program logic. A claim by CPI
measures about 173,000 compute units.
Gating on shielded funds
A program can require proof of funds by calling attest through CPI in the
same transaction as the action it gates. If the call returns, some note held
at least threshold at that moment; the tag in the arguments is the
holder's pseudonym under your scope, stable across calls.
Choose a scope per purpose — your program id plus a round or epoch — and record tags if one holder should act only once per scope. Remember what a tag is: a pseudonym of a key, not a count of funds. Attestations and receipts explains the difference.
Conditional ownership
Returnable notes make the claimant a Solana address — which may also be a PDA. A note that a program may take, or that returns to its sender after a date is an escrow with a timeout, with no escrow account and no public amounts.
Wire format and accounts: Protocol.