All articles
solanaspl-tokenpinocchioperformance

p-token: how Solana rewrote its most-called program (4,645 CU → 76)

Anza rewrote SPL Token in Pinocchio — same program ID, same accounts, but a transfer dropped from 4,645 CU to 76. The rewrite, the rollout, and the gotchas.

SPL Token is the most-called program on Solana — token operations consumed roughly 10% of the compute units in every block, despite being conceptually trivial. The cost wasn't the logic; it was the overhead of the standard Solana program model: deserialization into owned Rust structs, reference-counted account wrappers, the solana-program dependency tree, even the per-instruction log line.

p-token (proposed as SIMD-0266) is the from-scratch reimplementation in Pinocchio. Byte-for-byte compatible instruction set and account layouts, deployed under the same canonical program ID — and a transfer that used to cost 4,645 CU now costs 76.

The real numbers (SIMD-0266 benchmarks)

text
Instruction          p-token    SPL Token    Reduction
──────────────────────────────────────────────────────────
transfer             76 CU      4,645 CU     ~98%
transfer_checked     105 CU     6,200 CU     ~98%
initialize_mint      105 CU     2,967 CU     ~96%
initialize_account   154 CU     4,527 CU     ~97%
mint_to              119 CU     4,538 CU     ~97%
burn                 126 CU     4,753 CU     ~97%
approve              124 CU     2,904 CU     ~96%
close_account        120 CU     2,916 CU     ~96%
freeze_account       146 CU     4,265 CU     ~97%
sync_native          61 CU      3,045 CU     ~98%

These aren't "a few times cheaper" — they're 50-60x cheaper on the hot paths. For perspective: the old program's single Program log: Instruction: Transfer line alone cost ~103 CU — more than the entire p-token transfer (76 CU). The logging overhead exceeded the new program's full execution.

What stayed identical

  • Program ID. TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA — the swap happened via the Upgradeable BPF Loader at an epoch boundary. Every existing mint, ATA, and CPI caller works unchanged.
  • Instruction discriminators. Transfer is still 3, MintTo still 7. Existing serialized instructions parse the same way.
  • Account schemas. The 82-byte Mint and 165-byte Account layouts are byte-for-byte identical.

Why Pinocchio is this much cheaper

Pinocchio — created by Fernando "Febo" Otero at Anza — throws out the standard program model entirely:

  1. Zero-copy. Accounts are typed pointers into the runtime's input buffer, not heap-allocated structs. No deserialize-into-owned-struct, no re-serialize on write.
  2. no_std, zero external crates. Nosolana-program dependency tree, no standard library. Less code compiled in, less code executed per call.
  3. No smart pointers. The standard entrypoint wraps accounts in Rc<RefCell<>>. Pinocchio eliminates that refcounting overhead.
  4. Stack allocation. Fixed-size arrays instead of heap Vecs. A macro can forbid heap allocation entirely at compile time.

The result also shrank the binary: 131 KB → 95 KB, a 27% reduction.

SIMD-0266 also added three new instructions

This is the part most "it's just a rewrite" summaries miss — p-token is not purely the old instruction set. SIMD-0266 introduced three:

  • batch (discriminator 255) — bundles multiple token instructions into one CPI invocation, amortising the per-entry cost. The biggest lever for programs that do many token ops per transaction.
  • withdraw_excess_lamports (38) — recovers SOL accidentally sent to mint and multisig accounts.
  • unwrap_lamports (45) — direct lamport withdrawal from native (wrapped SOL) token accounts.

The one thing that breaks: log-scraping indexers

The old program emitted a log line per instruction — Program log: Instruction: Transfer. p-token omits these (the log was ~103 CU of pure overhead). Any indexer that detected token transfers by string-matching logs silently stops seeing them.

typescript
// BROKEN after p-token — the log line no longer exists
const isTransfer = tx.meta.logMessages.some(
  (l) => l.includes("Instruction: Transfer")
)

// CORRECT — decode the instruction data against the program's IDL
import { identifyTokenInstruction, TokenInstruction } from "@solana-program/token"
for (const ix of tx.transaction.message.instructions) {
  if (ix.programId.equals(TOKEN_PROGRAM_ID)) {
    const kind = identifyTokenInstruction(ix.data)   // decode by discriminator
    if (kind === TokenInstruction.Transfer) { /* … */ }
  }
}

If you run an indexer, a webhook pipeline, or analytics that depend on token-program logs, this is the migration to do. Decode instruction data by discriminator, never log-scrape.

How it was verified before going live

Replacing the most-called program on a $-billions chain is not a casual deploy. The verification stack:

  • Neodyme differential testing. Replayed essentially every mainnet transaction that ever touched the token program through both the old and new programs against real chain history. Result: zero divergences across months of testing. Over one sample window (Aug 3-11, 2025) the new program would have saved 8.9-9.1 trillion CUs — about 12% of the entire chain's blockspace.
  • Zellic audit. 8 findings — one Critical, three High — including out-of-bounds reads in memory helpers and unsafe pointer issues. All fixed.
  • Asymmetric Research. Found a loss-of-funds bug in the new batch instruction involving deferred ownership checks and fake native-token-account manipulation before runtime validation. Fixed.
  • Anza's internal differential fuzzer + Firedancer fuzzing.

The batch bug is the cautionary tale: the one genuinely new instruction was where the one loss-of-funds bug lived. The byte-compatible rewrite of existing instructions was provably safe; the net-new surface needed the most scrutiny.

The rollout

text
1. Verified bytecode staged at:
   ptok6rngomXrDbWf5v5Mkmu5CEbB51hzSCPDoj9DrvF
2. Released behind feature gate:
   ptokFjwyJtrwCa9Kgo9xoDS59V4QccBGEaRFnRPnSdP
3. Passed a supermajority validator stake vote (early 2026)
4. Activated at an epoch boundary — epoch 971, spring 2026
5. Required client versions: Agave v3.1.7+ / Firedancer v0.812.30108+

Built by Febo and Jon Cinque at Anza. The swap went through the Upgradeable BPF Loader at the epoch boundary — no frozen upgrade authority required, because the feature gate gated activation on validator consensus rather than a unilateral upgrade.

What to actually do

App developers: nothing — your calls to the Token program work unchanged and just got ~98% cheaper.

High-throughput protocols: re-measure your CU usage and lower your setComputeUnitLimit. Solana charges priority fees on allocated CUs, not used CUs — if your budgets were sized against the old 4,645-CU transfer, you're now massively over-allocating and overpaying. A swap that touched the Token program 6-10× just reclaimed ~25-45k CUs.

Indexers / analytics: migrate off log-scraping to instruction-data decoding (see above). This is the one non-optional change.

New program authors: p-token is the best production reference for a real Pinocchio program under a load-bearing program ID. Read p-token/src/processor/ for the zero-allocation instruction handlers.

What it doesn't cover

  • Token-2022 is separate. p-token is the rewrite of the legacy Token program. Token-2022 (TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb) with its extensions is a different program; a Pinocchio rewrite of it is a separate effort.
  • Account semantics are unchanged. Authority checks, freeze rules, delegation — all behave identically. Only the implementation and (slightly) the instruction surface changed.

References

Same program ID, same accounts, ~98% cheaper, plus a batch instruction and one log-scraping gotcha. The most consequential Solana program change of the year, and most apps didn't have to lift a finger for it.