Token-2022 — Confidential Transfers

Offline Confidential Token Operations: What Can Actually Be Signed Without RPC?

10 min read
SolanaToken-2022Confidential TransfersDurable Nonces

Since 26 October, the Token-2022 command-line tool can sign eleven confidential-transfer operations with no RPC connection at all. Confidential transfers, withdrawals, configuring an account and applying a pending balance are not among them, and the reason is structural. The line between the two groups is not about privacy. It is about randomness.

Token-2022 #1534 added the offline commands, and #1601 tightened their arguments on 29 October. We rebuilt the offline flow at the transaction level on a local validator running mainnet’s Token-2022 binary: signed the operations in separate processes with no network, landed them later on durable nonces, and measured the limits the CLI enforces.


Why exactly these eleven

Offline signing with several signers works only if every signer builds the same message, byte for byte. The CLI’s completion step relies on it too: you run the same command again with each presigner’s --signer PUBKEY=SIGNATURE, it rebuilds the message, and the signatures must still verify. That is only possible when the inputs fully determine the bytes.

textConfidential commands at token-2022 4b0346a5
signable offline (public values only)        still online
approve-confidential-transfer-account        configure-confidential-transfer-account
enable-confidential-credits                  transfer --confidential
disable-confidential-credits                 withdraw-confidential-tokens
enable-non-confidential-credits              apply-pending-balance
disable-non-confidential-credits
deposit-confidential-tokens
harvest-withheld-confidential-tokens
enable-confidential-fee-harvesting
disable-confidential-fee-harvesting
update-confidential-transfer-settings
apply-pending-burn

Everything on the right carries something encrypted. A decryptable balance is encrypted with a fresh random nonce every time, and ElGamal ciphertexts and the zero-knowledge proofs over them use random openings, so two runs never produce the same bytes. Most of them also need current chain state: a confidential transfer starts by reading the sender’s account over RPC. The first version of #1534 also signed proof-carrying operations such as mints, burns and withdrawals offline; the reviewer pointed out that re-running with --signer could never reproduce those messages, and the merged version kept only the deterministic commands.


What you supply instead of RPC

Online, the CLI looks things up. Offline it cannot, so the commands require them as arguments, most of them checked when the command line is parsed since #1601:

Which token program: --program-2022all eleven
Deposit: --mint-decimals and an amount (ALL is refused)deposit
Approve: the mint and the account’s addressapprove
Settings: both --approve-policy and --auditor-pubkeyupdate
Harvest: the accounts to harvest fromharvest
A blockhash: --blockhash, ideally with --nonceall eleven

The program flag is enforced because without RPC the CLI cannot see which program owns the mint. Deposits need decimals and an exact amount because the instruction carries both. A settings update rewrites both values, and online the CLI fills in whichever you leave out from the mint; offline it cannot. Harvest sources are sorted and de-duplicated, so co-signers who list them in a different order still build the same message.

Two quieter differences. With a supplied blockhash the CLI skips compute-unit simulation, so the transaction carries no compute-unit limit unless you set one. And a plain --blockhash expires about a minute after it was issued, so for signers who are actually apart, a durable nonce is the practical choice. The token client puts the nonce advance first and the compute-budget instructions last.


Signing apart, landing later

On a local Agave 4.2.1 validator with mainnet’s Token-2022 program cloned in, we set up a mint that requires account approval and a configured confidential account holding 1,000 public tokens. Then, from inputs fetched once, our own client built two transactions with no network access, each on its own durable nonce with a separate nonce authority: T1 approves the account; T2 deposits 250 tokens into its pending balance and disables non-confidential credits. The owner’s key signed in one process, the fee payer and nonce authority in another.

Messages built by the two processesidentical
T2 sent before T1 (preflight)rejected
T2’s nonce after that rejectionunchanged
T1, then T2both landed
Public balance after the deposit750
T2 sent againnonce used

T2 sent first failed the approval check, custom error 24, “Account not approved for confidential transfers”, and because preflight caught it the nonce survived and T2 stayed valid. Two rules fall out of this. The submitter, not the signers, owns the order. And each independent offline transaction needs its own nonce account, since the next nonce value is unknown until the previous transaction lands.

Without preflight the same mistake is expensive. We signed a deposit into a second, unapproved account and sent it with skipPreflight: it landed as failed, paid 15,000 lamports for its three signatures, and advanced its nonce, so the signed transaction was dead and had to be signed again.


The harvesting limits are packet sizes

Harvesting withheld confidential fees takes one writable account per source, 33 bytes each with its index, and a transaction must fit in 1,232 bytes. The CLI caps an offline harvest at 25 sources, or 23 with a durable nonce, and tells you to split larger sets. We serialized the harvest transaction in each layout and found the real limits:

textMost sources that fit in 1,232 bytes
fee payer only                                31
plus both compute-budget instructions         29
plus a nonce, fee payer as nonce authority    26
plus a nonce with a separate authority        23

The 23 is exact: 23 sources made a 1,217-byte transaction that the node accepted, and 24 made 1,250 bytes, which it refused as too large before simulating anything. The 25 without a nonce leaves headroom, for example for a memo. Offline the CLI does not split for you; one invocation is one transaction.


Signed is not the same as landable

Our first run used the Token-2022 program bundled with the test validator, and every deposit failed with “invalid instruction data”, including one made by the installed online CLI. Agave 4.2.1’s test validator ships Token-2022 10.0.0, and the binary it ships was built without the zk-ops feature, which in v10 and v11 gates the confidential operations that move balances. Without it the program rejects them before reading any account. Three of the eleven offline commands are among them: deposit, fee harvesting and applying a pending burn.

That makes the flag easy to detect: simulate a deposit with made-up accounts, and a build without it answers “invalid instruction data” while a build with it gets as far as checking the accounts. Mainnet’s Token-2022, last deployed on 23 October, got as far as the accounts, and so did devnet’s and testnet’s, last deployed on 23 September. Version 12, tagged on 28 October, has no such feature. Test offline flows against the program your cluster runs; cloning it into the test validator is one command.


Getting it

No released spl-token has these commands yet: the newest CLI release on 30 October is 5.7.1, from 23 October, before #1534 merged. Using them means building the CLI from the repository’s main branch. #1601 also moved the usage notes out of the CLI’s README; its author plans to put them on the documentation site with the rest of the confidential CLI documentation. The online operations depend on the proof program, which has a kill switch of its own.


What this does not show

We did not run the new CLI. Our client built the same instructions from the Token-2022 interface at 4b0346a5 and the same instruction order as the token client; the pull request’s own tests run the actual CLI against an unreachable RPC endpoint and a private validator. Our validator was Agave 4.2.1 with mainnet’s Token-2022 binary cloned in. We used one owner, no multisig, and no auditor.


Offline Confidential Signing: Questions People Actually Ask

Can a Solana confidential transfer be signed offline?

Not with the Token-2022 CLI. A confidential transfer carries freshly encrypted amounts and zero-knowledge proofs built with random values, and the CLI reads the sender’s encrypted balance from the chain before building it. A second signer could never rebuild the same message, so the offline support added on 26 October 2026 leaves transfers, withdrawals, account configuration and applying a pending balance online.

Which confidential operations can spl-token sign offline?

Eleven: approving an account, enabling or disabling confidential and non-confidential credits, depositing, harvesting withheld confidential fees to the mint, enabling or disabling that harvesting, updating the mint’s confidential settings, and applying a pending burn. All of them contain only public values, so the same inputs always produce the same message.

What does the CLI need offline that it normally reads over RPC?

The Token-2022 program flag, the mint’s decimals and an explicit amount for deposits, the mint address for approvals, both the approval policy and the auditor key for settings updates, the list of fee accounts to harvest, and a blockhash: a durable nonce, or a recent blockhash that expires in about a minute. It also skips compute-unit simulation, so no limit is set unless you pass one.

How many accounts can one offline confidential fee harvest include?

The CLI allows 25 offline, or 23 with a durable nonce. 23 is the exact maximum for a transaction with a separate nonce authority and both compute-budget instructions; in our test 24 sources made a 1,250-byte transaction that the node refused as too large. Larger sets have to be split across invocations.

Why does a confidential deposit fail with “invalid instruction data” on solana-test-validator?

Because the Token-2022 binary bundled with it may be built without the zk-ops feature. Agave 4.2.1’s test validator ships Token-2022 10.0.0 in such a build, and it rejects deposits, transfers, withdrawals and pending-balance operations before reading any account. Mainnet’s deployed Token-2022 accepts them. Clone the cluster’s program into the test validator, or use a build with the feature, to test confidential flows.


The one-line version

Token-2022’s CLI can sign the eleven confidential operations whose bytes depend only on public inputs, so signers can work apart and land them later on durable nonces, while anything carrying an encrypted balance or a proof stays online because its randomness can never be reproduced.

Sources: Token-2022 #1534 and #1601, the repository at 4b0346a5, the v10.0.0 and v12.0.0 tags and the CLI 5.7.1 release; solana-zk-sdk 8.0.1; Agave 4.2.1’s bundled programs. Measurements are from our own client against a local validator, plus simulations against mainnet, devnet and testnet.

xroot.devOn-chain research, published in full
𝕏 @xrootdev
← Back to all notes