CLI — Solana × Transaction V1

Agave CLI Transaction V1: Which Signing Workflows Break?

10 min read
SolanaCLITransactionsHardware Wallets

On 29 October, Agave merged a change that makes the solana command-line tool build every transaction as a version 1 message. Not optionally and not behind a flag: there is no longer a code path in the CLI that compiles a legacy message.

Its changelog entry carries one warning: hardware wallets that cannot parse v1 “may no longer work with the CLI”. That is the visible break. There are three quieter ones, plus a list of things that look like they should break and do not. We traced each through the code at the merge commit and measured the ones with numbers by compiling the same messages the new CLI compiles.


One builder, one version

Pull request 16448 replaces hand-built message assembly across eleven files with a MessageBuilder, whose build returns VersionedMessage::V1 and nothing else. Three things change in what the CLI emits, all because the v1 format moves resource requests out of ComputeBudget instructions and into the message itself:

First byte of a transfer message0x01 → 0x81
Same transfer, message size150 → 164 bytes
Compute limit when not simulated200,000 per instruction, max 1.4M
Loaded-accounts data limitalways 64 MiB
Priority feeflat: ⌈price × limit ÷ 10⁶⌉ lamports

The explicit limits are not optional either. A v1 transaction that requests no compute limit and no loaded-accounts size gets zero of both and fails at execution, so the builder always writes them. The rest of this post is what those three facts do to the ways people sign.


The Ledger app refuses them, and fails safe

The CLI side of Ledger signing does not care about message versions: it chunks the message bytes into sign APDUs and lets the device decide. The device runs the Solana app, version 1.16.1 on the day of the merge, and its parser accepts any first byte with the high bit set as “a versioned message”, then reads the rest with the v0 layout. Walk the CLI’s v1 transfer through it:

textv1 transfer, read as v0
81            version byte          → versioned, version 1
01 00 01      header                → 1 signer, 0 + 1 read-only
0c            [v1: config mask]     → read as "12 account keys"
              12 × 32 = 384 bytes needed, 159 left
                                    → check_buffer_length fails
device → 0x6a80  CLI → "Ledger received invalid Solana message"

For a small message the misread account count runs off the end of the buffer. A large v1 message can survive that read, but the next step looks for your public key among the first “accounts”, and in a v1 message those bytes are the config mask and the lifetime, never your key. Depending on where the misread runs out, the device answers 0x6a80 or 0x6a81. The app’s sign handler returns on both errors before it reaches the blind-signing branch, so this fails closed: the device refuses rather than showing wrong amounts or offering to sign a hash.

A reviewer on the pull request put the cost plainly: Ledger has not implemented v1, and when it does, the change will “force all users to update their Ledger firmware”. The suggested workaround is to install an earlier CLI. We read the Ledger app’s source rather than signing on a device, and did not check Trezor or Keystone firmware, which the CLI also supports.


Offline signing across CLI versions

Offline signing with --sign-only works because every party rebuilds the same message from the same arguments and signs the same bytes. The submitter attaches the collected signatures as --signer PUBKEY=SIGNATURE, and each is checked against the message it compiles.

A cold signer on a 4.4 CLI compiles the legacy encoding; a submitter on a build with this change compiles v1. Same instructions, same blockhash, different bytes. We signed the legacy encoding of a transfer and checked the signature against both: it verifies the legacy message and fails the v1 one. Multi-party flows, treasury setups and any procedure that moves an unsigned transaction between machines need every participant on the same side of the change.

Durable nonces, the usual companion of offline signing, keep working on their own. The builder inserts AdvanceNonceAccount as the first instruction and uses the nonce value as the message lifetime, and the runtime finds a durable nonce the same way for every message version: instruction zero, the System program, the advance-nonce discriminant. The problem is not the nonce. It is that the workflows nonces exist for are the ones that cross machines, and often end on a hardware wallet.


The 44× priority fee on nonce transactions

Online commands simulate first: when you pass --with-compute-unit-price, the CLI simulates the transaction and sets the limit to what it consumed, then recomputes the v1 priority fee from that. Those fees barely move. A transaction that cannot be simulated, because it uses a durable nonce, takes the default limit, and that is where the two versions part.

A legacy message with no limit instruction gets the runtime’s default, which gives each non-migrating builtin instruction 3,000 compute units. The System program and the ComputeBudget program are both builtins, so a nonce transfer with a price, three instructions, defaulted to 9,000. The v1 builder has no builtin discount: 200,000 per instruction for the two it emits.

Nonce transfer, legacy limit9,000 CU
Nonce transfer, v1 limit400,000 CU
Priority fee at 1,000,000 micro-lamports/CU9,000 → 400,000 lamports
Priority fee at 100,000 micro-lamports/CU900 → 40,000 lamports

Same flag, 44 times the fee. solana transfer does not take a compute-limit flag, so the only lever is the price: divide it by the ratio of the limits to pay what you paid before. Without a price there is no priority fee at all, but the declared limit still grows. An online transfer without a price is never simulated, so its v1 message declares the 1,400,000-unit maximum where the legacy one defaulted to 3,000. That costs nothing in fees. It does raise the cost the leader divides by when it ranks transactions.


What looks like it should break, and does not

  • Address lookup tables. v1 has none, but the CLI never used them for its own messages, and the address-lookup-table subcommands are ordinary instructions to the lookup-table program, so they build and send as v1 like anything else.
  • Program deploys get no faster. v1 allows 4,096-byte transactions, but the loader limits write instruction data to the 1,232-byte packet size for every version. v1’s larger header costs each write 13 bytes of program data, 1,012 down to 999 when one keypair pays for and owns the buffer, so an 800 KB program takes 801 writes instead of 791. With a price set, deploy writes are simulated in both versions and their fees stay close.
  • Memos. --memo already used the Memo v4 program before the change, and still does.
  • Durable nonces themselves, as section 03 shows.

One more downstream effect belongs to everyone who reads transactions rather than signing them. CLI transactions now carry their compute settings in the v1 config rather than in ComputeBudget instructions, so fee dashboards that scan for those instructions will record nothing for them. That is the general v1 reader problem, arriving through a tool many teams treat as a reference client.


What to do before it ships

No release contained the change when it merged; it is on master, on the way to the next minor line. Until Ledger ships v1 parsing, pin a 4.4 CLI on any machine that signs with a hardware wallet. Upgrade every machine in an offline-signing procedure together, never one at a time. And if you set a price on nonce or other unsimulated transactions, recompute it, because the limit it multiplies is no longer the runtime default.


What this does not show

The Ledger result comes from reading the app’s parser and walking the CLI’s bytes through it, not from signing on a device. The sizes, signature check and limits come from compiling the same messages with the solana-message 6.0.0 crate the merge commit uses; the legacy defaults come from the runtime’s builtin table. Which other CLI commands use default rather than simulated limits varies by subcommand, and only the transfer path is worked through here.


CLI Transaction V1: Questions People Actually Ask

Does the Solana CLI still create legacy transactions?

Not on Agave master after 29 October 2026. Pull request 16448 routes every CLI message through a MessageBuilder that compiles version 1 messages only. Released CLIs up to 4.4 still build legacy messages, and no release contained the change when it merged.

Can I sign v1 transactions with a Ledger?

Not with Ledger Solana app 1.16.1. Its parser reads a v1 message with the v0 layout and rejects it, so the CLI reports “Ledger received invalid Solana message”. It fails closed: the device refuses rather than displaying wrong details or offering blind signing.

Do durable nonce transactions still work in v1?

Yes. The builder puts AdvanceNonceAccount first and uses the nonce value as the message lifetime, and the runtime detects durable nonces the same way for every message version. What changes is the priority fee: nonce transactions use the default compute limit, which the v1 builder sets at 200,000 per instruction.

Why did my priority fee go up with the new CLI?

v1 carries a flat lamport priority fee, which the CLI computes as price × limit ÷ 1,000,000. Commands that do not simulate, such as transfers with a durable nonce, now declare 200,000 compute units per instruction instead of the runtime’s 3,000 for each builtin instruction, so the same --with-compute-unit-price costs 44 times more on a nonce transfer.

Can I sign offline with one CLI version and submit with another?

Not across this change. Offline signing only works if every signer and the submitter compile byte-identical messages. A signature over the legacy encoding does not verify the v1 encoding of the same instructions, so everyone in the flow has to run a CLI on the same side of the change.


The one-line version

The CLI change is a format change, and formats break at the edges: the device that parses them, the other machine that must produce the same bytes, and the fee that is now computed from a different number. Keep hardware-wallet machines on 4.4, move offline-signing setups together, and recompute prices on nonce transactions.

The v1 wire format itself is in Solana’s 4,096-byte transactions, durable nonces are in the pre-signed transaction that never expires, and what the limit does to landing is in the leader’s ranking formula.

Sources: Agave at the merge of #16448, commit a4f4c5ea9e: message_builder.rs, compute_budget.rs, wallet.rs, program.rs, remote-wallet/ledger.rs, compute_budget_instruction_details.rs and builtins-default-costs; LedgerHQ/app-solana at af6ed454f0: parser.c and handle_sign_message.c.

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