feat(compression/cnft-vault): add pinocchio example - #722
Conversation
Ports the Anchor cnft-vault example to Pinocchio: a vault PDA holds compressed NFTs by being their leaf owner, and signs mpl-bubblegum `Transfer` CPIs to send them back out — one at a time, or two from different trees in one instruction. Both proofs of `withdraw_two_cnfts` arrive concatenated in the account tail, so the instruction data carries each proof's length to split them. Unlike the Anchor version, which ignores the second length, this checks that the two lengths account for exactly the proof accounts supplied — otherwise an overstated first length would let the second transfer read accounts the first already consumed. The proof is variable-length, so the CPI account list is built into a fixed-size stack array and passed with `invoke_signed_with_bounds` rather than the const-generic `invoke_signed`. Unlike the Anchor variant, whose tests need devnet and a DAS indexer, the LiteSVM suite runs entirely locally against the mainnet-dumped bubblegum, account-compression and noop programs. Each cNFT gets its own tree so every proof stays the empty-node path, and each withdrawal is checked by recomputing the expected leaf off-chain against the tree's own state.
Greptile SummaryThe PR adds a Pinocchio implementation of the compressed-NFT vault, supporting single and paired withdrawals through Bubblegum CPIs.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Reviews (2): Last reviewed commit: "cnft-vault: raise the proof cap to the p..." | Re-trigger Greptile |
A 24-node cap could reject a valid proof: proofs are `max_depth - canopy_depth` nodes and SPL Account Compression allows a max_depth of 30, so a canopy-less deep tree needs all 30 — and address lookup tables make that transaction fit. At 30 the bound can no longer reject anything valid; it only keeps the CPI account list on the stack.
|
@amilz could you take a look at this one when you get a chance? No open review threads left on it, so it is ready for maintainer review. It is one of 23 open Pinocchio ports I have up — they are independent and self-contained, so they can be reviewed and merged in any order: https://github.com/solana-developers/program-examples/pulls/MarkFeder |
|
Hey, in order to not overflow our reviewers with PRs, we'll limit the number of opened PRs to 3 per contributor for now. Thanks for the work! (You can re-open those whenever your other PRs get merged in) |
Ports
compression/cnft-vaultto Pinocchio — the second of the threecompression/examples, after #719.Program
A vault PDA (
[b"cNFT-vault"]) holds compressed NFTs by being their leaf owner, and signs mpl-bubblegumTransferCPIs to send them back out. Two instructions, dispatched on a leading discriminator byte:withdraw_cnft— one cNFT to one recipientwithdraw_two_cnfts— two cNFTs, possibly from different trees, in a single instructionBoth share one
transfer_cnfthelper. As in #719 the transfer arguments are already in bubblegum's wire order, so the CPI data is the discriminator followed by the arguments verbatim, and the variable-length proof means the account list is built into a fixed-size stack array and passed withinvoke_signed_with_boundsinstead of the const-genericinvoke_signed.One deliberate difference from the Anchor version.
withdraw_two_cnftsreceives both proofs concatenated in the account tail and splits them using lengths from the instruction data. The Anchor version takes both lengths but ignores the second (_proof_2_length), so an overstated first length would push the split past the first proof and let the second transfer read accounts the first already consumed. This checks that the two lengths sum to exactly the proof accounts supplied, before doing anything else. There's a test for it.The vault and tree-authority PDAs are both rederived on-chain, and the CPI always targets the hardcoded bubblegum id rather than the passed program account.
Tests
The Anchor variant is in
.ghaignorebecause its tests need devnet and a DAS indexer. This one runs entirely under LiteSVM against the mainnet-dumped bubblegum, account-compression and noop programs:withdraw_cnftfrom the first treewithdraw_two_cnftsfrom the other twoInvalidInstructionDataGiving each cNFT its own tree keeps every proof to the empty-node path, so the test needs no merkle tree implementation of its own. Each withdrawal is verified by recomputing the expected post-transfer leaf — a transfer rewrites the leaf with the recipient as both owner and delegate — and comparing it to the tree's own change log, rather than just checking the transaction succeeded.
Verification
cargo build-sbf, isolated and workspaceclippy -D warnings,cargo fmt --check --all,tsc --noEmit,prettier --check, and the 4-test LiteSVM suite all pass locally.