Skip to content

feat(compression/cutils): add pinocchio example - #723

Open
MarkFeder wants to merge 1 commit into
solana-foundation:mainfrom
MarkFeder:compression-cutils-pinocchio
Open

feat(compression/cutils): add pinocchio example#723
MarkFeder wants to merge 1 commit into
solana-foundation:mainfrom
MarkFeder:compression-cutils-pinocchio

Conversation

@MarkFeder

@MarkFeder MarkFeder commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

What

Adds a Pinocchio implementation of the cutils example, alongside the existing anchor version. This is the last un-ported example under compression/.

Both instructions are carried over:

  • mint — CPIs into mpl-bubblegum's MintToCollectionV1 to mint a compressed NFT into a Metaplex collection. The metadata is fixed (BURGER / BURG, one creator at 100%) apart from the URI, which is the instruction data.
  • verify — recomputes the asset ID and the LeafSchema::V1 hash, then CPIs into SPL Account Compression's verify_leaf to prove the leaf is in the tree. Nothing is written; it either succeeds or the CPI fails.

How it works

Neither bubblegum nor account-compression has a Pinocchio crate, so both instructions are built by hand:

  1. MetadataArgs is borsh-encoded straight into a stack buffer sized for the longest URI Token Metadata accepts (200 bytes), so the mint needs no allocator. A small Writer cursor handles the length-prefixed strings.
  2. The leaf hash uses the keccak syscall directly (sol_keccak256), matching LeafSchema::V1::hash() byte for byte — version tag, asset ID, owner, delegate, nonce, data hash, creator hash.
  3. The proof is variable-length, so verify uses invoke_with_bounds with the account list capped at 30 — the protocol maximum max_depth, so no valid proof can exceed it.
  4. Both CPIs target hardcoded program IDs, never a caller-supplied program account. The tree authority is rederived on-chain so a bad one fails early rather than deep inside the CPI.

Test

The anchor version can't run in CI — it needs devnet and a DAS indexer, which is why compression/cutils/anchor sits in .ghaignore and its Verify test is commented out. The Pinocchio version runs entirely on LiteSVM against the four mainnet programs prepare.mjs dumps (bubblegum, account-compression, noop, token-metadata).

The test builds a real Metaplex collection — mint, ATA, CreateMetadataAccountV3 with collection details, CreateMasterEditionV3 — creates a merkle tree, mints through the program, then asserts the tree's own root equals the root recomputed from a leaf hash built independently in TypeScript. That check is what pins down the metadata serialization, both hashes and the leaf schema.

Compressed NFT Utils (Pinocchio)
  ✔ Mints a compressed NFT into the collection
  ✔ Verifies the compressed NFT belongs to the tree
  ✔ Rejects a proof that does not lead to the root
  ✔ Rejects a verify by someone who does not own the leaf
  ✔ Rejects a verify the leaf owner did not sign
  ✔ Rejects an empty URI
6 passing

Verified locally: cargo build-sbf, cargo fmt --check, Clippy (-D warnings), cargo test, tsc --noEmit, Prettier, and pnpm install --frozen-lockfile all clean.

Note

The anchor example's Data state account is not carried over — nothing in either action constructs or reads it, so it is dead code there. Happy to port it if you'd rather keep the two in lockstep.

prepare.mjs uses solana program dump -um rather than solana config set -um, per #720.


AI use: I chose the approach (hand-built CPI wire formats, stack-allocated borsh encoding, the LiteSVM collection setup) and verified the discriminators and hashing against the mpl-bubblegum and spl-account-compression sources; implementation and tests were written with Claude Code and reviewed by me.

@MarkFeder
MarkFeder requested a review from dev-jodee as a code owner September 2, 2026 10:27
@greptile-apps

greptile-apps Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds a Pinocchio implementation of the compressed NFT utilities example, including hand-built Bubblegum and account-compression CPIs.

  • Adds mint and leaf-verification instructions to the workspace.
  • Adds LiteSVM integration tests and setup scripts for the required mainnet programs.
  • Adds the TypeScript and Rust dependencies needed to build and exercise the example.

Confidence Score: 4/5

The PR does not yet appear safe to merge because its test dependency requires Node 24 while the repository remains configured for Node 22.

The unresolved runtime contract mismatch remains: the lockfile selects @solana-program/token@0.15.0 with a Node 24 minimum, but the repository declares Node 22.

Files Needing Attention: compression/cutils/pinocchio/package.json, compression/cutils/pinocchio/pnpm-lock.yaml

Important Files Changed

Filename Overview
compression/cutils/pinocchio/program/src/instructions/mint.rs Implements stack-allocated Bubblegum metadata serialization and the compressed-NFT mint CPI.
compression/cutils/pinocchio/program/src/instructions/verify.rs Reconstructs the Bubblegum leaf hash and invokes account-compression leaf verification with bounded proof accounts.
compression/cutils/pinocchio/tests/test.ts Builds a real collection and Merkle tree in LiteSVM and tests minting, verification, and rejection paths.
compression/cutils/pinocchio/package.json Adds the example's JavaScript dependencies, including a token client whose declared Node requirement remains incompatible with the repository runtime.
compression/cutils/pinocchio/pnpm-lock.yaml Locks the TypeScript dependency graph and still resolves the direct token client to a Node-24-only release.

Reviews (2): Last reviewed commit: "feat(compression/cutils): add pinocchio ..." | Re-trigger Greptile

Comment thread compression/cutils/pinocchio/package.json
@MarkFeder

Copy link
Copy Markdown
Contributor Author

Real inconsistency, but not one this PR introduces and not one the suggested fix resolves.

Every @solana-program/token release that declares engines requires Node 24 — 0.14.0, 0.15.0 and 0.16.0 are all >=24.0.0. Versions ≤0.13.0 declare no engines at all, and predate the API this test uses. So there is no Node 22-compatible version to select.

It is already the status quo on main, in 8 merged packages — tokens/create-token/{native,pinocchio}, tokens/escrow/{native,pinocchio}, tokens/nft-operations/pinocchio, tokens/external-delegate-token-master/anchor, tokens/merkle-tree-token-claimer/anchor. Several are already on ^0.15.0, the version here.

The root .nvmrc is not consumed by any workflow. just.yml reads ${{ matrix.project }}/.nvmrc — the per-project files under games/world-cup and games/gacha. The Pinocchio workflow calls .github/actions/setup with no node-version-file, so it takes the default node-version: 'lts/*' with check-latest: true, which is Node 24. That is why all 29 checks pass.

Leaving the dependency as-is to match the merged examples. Bumping the root .nvmrc to 24 is worth doing, but it is repo-wide and belongs in its own PR — happy to open one.

@MarkFeder

Copy link
Copy Markdown
Contributor Author

Split the runtime bump out to #724 as offered — CONTRIBUTING.md asks for runtime bumps to be deliberate rather than incidental, so it stays out of this PR.

Ports both instructions. `mint` CPIs into mpl-bubblegum's
`MintToCollectionV1` and `verify` rebuilds the leaf hash and CPIs into
SPL Account Compression's `verify_leaf`.

Neither program has a pinocchio crate, so the instruction data is built
by hand. `MetadataArgs` is borsh-encoded straight into a stack buffer
sized for the longest URI Token Metadata accepts, which keeps the mint
allocation-free. The leaf hash uses the keccak syscall directly.

The anchor version cannot run in CI — it needs devnet and a DAS indexer,
which is why it sits in .ghaignore, and its verify test is commented out.
This one runs on LiteSVM against the four mainnet programs prepare.mjs
dumps: it builds a real Metaplex collection, mints into it through the
program, and asserts the tree's own root matches the leaf hash recomputed
in TypeScript. Both verify failure modes are covered.

The anchor example's `Data` account is not carried over: nothing
constructs or reads it.
@MarkFeder
MarkFeder force-pushed the compression-cutils-pinocchio branch from d33bf16 to e0e0f6c Compare September 2, 2026 10:52
@MarkFeder

Copy link
Copy Markdown
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant