Skip to content
VOUCHVOUCHDocs

Guides

Reserves

Prove a treasury’s shielded reserves as one batch, check it, and publish it to a Solana feed that a token can only mint within.

On this page

#1 · Prove a batch

A reserves batchnote 1note 2note 3note 4note 5Proof 1notes 1, 2tags t₁ t₂Proof 2notes 3, 4tags t₃ t₄Proof 3note 5tag t₅verify_batcheach member verifiesone scope · one anchorno tag twiceone batched Halo2 checktotal = Σ thresholdsValid batch4,018.125 ZECacross 3 proofsInvalid batcha tag repeats:a note counted twice
Every usable note goes into a proof of at most five notes; all proofs share one scope and one anchor. The verifier checks each member, refuses any tag seen twice, and states the sum. This is the demo batch: five notes, two per proof.

One proof covers up to five notes. A batch covers every usable note: several version 2 proofs in one scope, against one anchor, in a .pofb file. A request of kind reserves makes a batch by default; --batch-size sets how many notes go in each proof.

Shell
pof-prove prove --request <encoded> --snapshot target/snapshots/mainnet-<height>.vsnp \
                --seed-file ~/.vouch/seed.txt --batch-size 5 --out reserves.pofb

#2 · Check it

The verifier checks every member, refuses a batch in which any tag appears twice (a note counted twice), and states the sum. Natively the members’ Halo2 proofs are checked together with one batched multi-scalar multiplication, falling back to one by one to name a bad member. Drop it on /reserves, or:

Shell
pof-verify check reserves.pofb --audience reserves:wzec-demo --anchors anchors.json
Output
Valid batch · 4018.12500000 at least, across 3 proofs · anchor 3491040
  proof 1   Valid
  proof 2   Valid
  proof 3   Valid
A batch that counts one note twice
Invalid batch · Proof 2 counts a note another proof in the batch already counted.
  proof 1   Valid
  proof 2   Valid

#3 · Publish the feed

Publishing reserves and minting against themTreasurypof-proveIssuerrelayer keyAttestor/v1/batchpof-reserveSolanaprove a batch--batch-size 5 → .pofbreserves.pofbPOST /v1/batch { attest }verify_batch, then signPOF-RESERVE-v1 · 112 Bmessage + signaturetx: Ed25519 ix + publishsigners · slot · expiryaudience · anchor ≥ currentFeed updated: total, members, heights, slotsecure_mint(amount)fresh? within total?supply + amount ≤ totalminted, or refused
The attestor signs a batch verdict; pof-reserve checks that signature by instruction introspection and stores the total. Its token then mints only within that total, and only while the feed is fresh.
The reserves message (POF-RESERVE-v1)domain"POF-RESERVE-v1"14 Bscopethe batch scope32 Baudienceblake2b(id)32 Btotalzatoshi8 Bmembersproofs2 Banchorheight4 Bnc_heightunmoved since4 Bexpiresfirst expiry8 Bslotfreshness8 B112 bytes
112 bytes, little-endian. The attestor signs it after verify_batch; pof-reserve::publish parses exactly this layout.

publish accepts the message only if enough distinct allowlisted attestors signed exactly it, the slot is recent, the proofs have not expired, the audience is the feed’s, and the anchor is not older than the one the feed already holds.

#4 · Secure Mint

The reserves feed's statesNever publishedmints nothingFreshmints within the totalrefuses past itStalemints nothinguntil a new publishpublishheartbeat passesor the proofs expirepublish a batch (anchor ≥ current)publish again: the heartbeat restarts
Secure Mint works only in the Fresh state, and only while supply plus the amount stays within the proven total. Freshness lapses when the heartbeat passes without a publish, or when the proofs behind the feed expire.

The feed’s token (its mint authority is the feed PDA) mints only when all of these hold:

  • the feed has been published at least once;
  • the last publish is within the feed’s heartbeat (the demo feed: 9,000 slots, about an hour);
  • the proofs behind it have not expired;
  • supply plus the amount stays at or under the proven total;
  • the caller is the feed’s issuer.

Try both breakers on /reserves: mint past the reserves, or wait out the heartbeat. Each is refused by the program, not by the site.

#Reading the feed

Any program can read the feed account, a PDA at [b"feed", audience] under pof-reserve (5Q26Tjcd4equ5te5hF15eG4xK6uvkbZtKkEeqdf535h9). The audience is blake2b-256("pof-audience:" ‖ id).

Rust
pub struct Feed {
    pub audience: [u8; 32],
    pub issuer: Pubkey,        // may call secure_mint
    pub mint: Pubkey,          // PDA [b"mint", feed]
    pub heartbeat_slots: u64,
    pub scope: [u8; 32],
    pub total_zatoshi: u64,    // the proven reserves
    pub members: u16,          // proofs in the batch
    pub anchor_height: u32,    // spent set "as of"
    pub nc_height: u32,        // unmoved since, when earlier
    pub expires_at: i64,
    pub updated_slot: u64,
    pub publishes: u32,
    pub bump: u8,
}

Treat the feed as stale exactly as the program does: no publish yet, more than heartbeat_slots since updated_slot, or past expires_at.

#What reserves prove, and what they do not

A feed proves the issuer controlled at least the total at the anchor, with no note counted twice within the batch. It does not prove liabilities (that half of proof of solvency is on the roadmap), and it does not lock the coins: a later publish with a lower total lowers what can be minted, but cannot claw back what was minted already.