Originally published on xroot.dev.
getFeeForMessage answers one question: what would this message pay? For years Agave also answered null when the message’s blockhash was gone or its durable nonce did not match, and code learned to read that null as “get a new blockhash”. Agave 4.5 stops doing it. And the null was never a reliable expiry signal in the first place.
Agave #16519, merged on 29 October, removes both checks, because the fee depends on neither. We ran the old behaviour on a local Agave 4.2.1 validator, which has the same check as the 4.4 release, and found a 150-block window in which it priced transactions that the same node was already rejecting with “Blockhash not found”.
What the fee is made of
The fee has two parts: 5,000 lamports for each signature, counting the signatures inside Ed25519, secp256k1 and secp256r1 precompile instructions, and the priority fee, the compute-unit limit times the compute-unit price in micro-lamports, divided by a million and rounded up. Both come from the message itself:
Transfer, one signature — 5,000
Two signatures — 10,000
200,000 CU at 1,000,000 µlamports — 205,000
200,000 CU at 1 µlamport — 5,001
Values in lamports, from our local validator. The last row is the rounding: 0.2 lamports of priority fee costs 1. How that fee ranks a transaction against others is a separate calculation. Nothing here depends on the recent blockhash or on a durable nonce.
What the old version checked first
Up to 4.4, Agave’s bank refused to price a message unless its blockhash was in the bank’s blockhash queue or, failing that, the message was a durable-nonce transaction whose nonce account stored exactly that value. Either check failing meant null. So did invalid compute-budget instructions, such as two SetComputeUnitLimit instructions. Three different problems, one answer.
The catch is in the queue. A bank keeps the last 300 blockhashes, but accepts a transaction only if its blockhash is at most 150 blocks old (blockhash_queue.rs, is_blockhash_valid). The fee check asked “is it in the queue?”, not “is it young enough?”. We held one blockhash on the local validator and recorded when each method gave up on it:
Blockhash issued at block height 360 — +0
lastValidBlockHeight 510 — +150 blocks
isBlockhashValid false, at height 511 — +151 blocks
getFeeForMessage still — 5,000
sendTransaction — Blockhash not found
getFeeForMessage null, at height 661 — +301 blocks
For 150 blocks, about a minute at mainnet’s 400 ms slots, the old method priced a transaction that the network rejected. Code that waits for a null before refreshing its blockhash spends that minute sending transactions that are dropped.
What Agave 4.5 changes
The new function reads the compute-budget instructions and computes the fee, nothing else. The changelog lists it under 4.5.0, which was unreleased at the merge, when the stable line was 4.3 and the beta 4.4. Side by side, with the old column measured on 4.2.1 and the new one read from the merged code:
Fresh blockhash — 5,000 → 5,000
Blockhash 151–300 blocks old — 5,000 → 5,000
Blockhash dropped or never seen — null → 5,000
Durable nonce, stored value — 5,000 → 5,000
Durable nonce, any other value — null → 5,000
Duplicate SetComputeUnitLimit — null → null
v0, lookup table missing — error → error
The last row is a JSON-RPC error, -32602 “invalid transaction message: Attempted to lookup addresses from a table that does not exist”: the RPC method resolves lookup tables before it asks the bank. The issue behind the change gives one more reason for it: the nonce check was the only part of the fee calculation that needed resolved addresses, which kept RPC from using a lighter message type. In 4.5 a null means one thing only: the compute-budget instructions are invalid. Like the 4.4 RPC changes, it will reach clients gradually, as RPC providers upgrade, so code meets both behaviours for a while.
What a fee does not promise
A fee is computed from the message alone, in every version. It never checked the things that decide whether a transaction lands. We signed and sent three transactions the local node had just priced at 5,000 lamports:
Fee payer with no account: “no record of a prior credit” — rejected
Transfer above the payer’s balance: custom program error 0x1 — rejected
Blockhash past its last valid height: “Blockhash not found” — rejected
All three were rejected in preflight simulation. A fee also says nothing about signatures, account locks, the compute the instructions will use or whether a program succeeds. From 4.5 it says nothing about the blockhash or the nonce either.
What to call instead
Is the blockhash still usable? getLatestBlockhash -> lastValidBlockHeight,
then getBlockHeight <= lastValidBlockHeight
(or isBlockhashValid)
Is the durable nonce current? getAccountInfo on the nonce account (jsonParsed):
its stored blockhash must equal the message's
Would it execute right now? simulateTransaction
What will it pay? getFeeForMessage
In our run isBlockhashValid first reported false at block height 511, one past the blockhash’s lastValidBlockHeight of 510, and sendTransaction rejected it there, so the height comparison matches what the node enforces. Simulation is the closest thing to a validity check, but it is one node’s view at one moment: an account can change between simulation and landing.
What this does not show
We measured Agave 4.2.1’s test validator, whose fee check matches the 4.4 release line for line in behaviour; we did not run 4.4 or 4.5, so the 4.5 column comes from the merged code. A local validator makes blocks at its own pace, so the 150-block window is in blocks, not seconds. Every call used processed commitment on a single node.
getFeeForMessage: Questions People Actually Ask
Does getFeeForMessage check whether a Solana blockhash is still valid?
No. From Agave 4.5 it does not look at the blockhash at all. Before that it only checked that the blockhash was still in the bank’s queue of the last 300, while transactions are accepted only up to 150 blocks, so it returned fees for blockhashes the network had already stopped accepting. Use the lastValidBlockHeight returned by getLatestBlockhash, or isBlockhashValid.
When does getFeeForMessage return null in Agave 4.5?
Only when the message’s compute-budget instructions are invalid, for example two SetComputeUnitLimit instructions, bad instruction data or an invalid heap size. A message that cannot be read at all, such as a v0 message whose address lookup table does not exist, gets a JSON-RPC error instead of null.
How long is a Solana blockhash valid for transactions?
150 blocks after the block that produced it. getLatestBlockhash returns lastValidBlockHeight, the last block height at which a transaction using it can land. In our test isBlockhashValid turned false at lastValidBlockHeight + 1, and sendTransaction rejected the blockhash from then on.
Does getFeeForMessage include the priority fee?
Yes. It adds the priority fee set by the message’s compute-budget instructions, compute-unit limit times compute-unit price in micro-lamports, divided by a million and rounded up, to 5,000 lamports per signature, counting the signatures in Ed25519, secp256k1 and secp256r1 precompile instructions too.
Does a fee from getFeeForMessage mean the transaction will succeed?
No. It is computed from the message alone. It does not check signatures, whether the fee payer exists or can pay, the blockhash or durable nonce (from Agave 4.5), account locks or whether the instructions succeed. Simulate the transaction to check those.
The one-line version
getFeeForMessage prices a message and nothing more: from Agave 4.5 it returns a fee for expired blockhashes and wrong nonces, and before that it kept pricing blockhashes for 150 blocks after the network stopped accepting them, so check expiry with lastValidBlockHeight and validity with simulateTransaction.
Sources: Agave #16519, merged as 8ffb33c9, and issue #15876; the 4.4.0 release source for the old check, the blockhash queue and the processing age. Measurements are from our own client against a local solana-test-validator 4.2.1.
Top comments (0)