feat(compression/cnft-vault): add pinocchio example - #722
Open
MarkFeder wants to merge 2 commits into
Open
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.
Contributor
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.
Contributor
Author
|
@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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.