Summary
Uploading a large metadata value (a ~372 KB Anchor IDL, ~64 chunk transactions) fails reliably
against a hosted devnet RPC, and leaves a full-size, rent-paid, partially written account
behind. It succeeds every time against a single-node solana-test-validator.
The cause is that the compute-unit estimation simulates each chunk write without a
minContextSlot, so a dependent read can be served by a node that has not yet seen the
transaction that created the buffer.
[Error] The provided transaction plan failed to execute.
SolanaError: Transaction simulation failed
cause: SolanaError: Invalid account data for instruction (instruction #3)
Program logs from the failing simulation:
Program ComputeBudget111111111111111111111111111111 invoke [1]
Program ComputeBudget111111111111111111111111111111 success
Program ComputeBudget111111111111111111111111111111 invoke [1]
Program ComputeBudget111111111111111111111111111111 success
Program ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S invoke [1]
Program ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S consumed 84 of 154 compute units
Program ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S failed: invalid account data for instruction
Two ComputeBudget instructions and then straight to the metadata program, with no System program
invoke — so this is not the transaction that creates the buffer. It is a chunk Write, failing
because the buffer is not visible to the node serving the simulation. loadedAccountsDataSize is
48 247, which is the metadata program alone; the 60 KB buffer was not loaded.
The ordering is not the problem
Worth stating, because it is the obvious first guess and it is wrong. Both of these were checked:
getCreateBufferInstructionPlan builds
sequentialInstructionPlan([CreateAccount, Allocate, SetAuthority, parallelInstructionPlan([writes])]),
and the planner preserves it. Dumping the resulting transaction plan gives
sequential [3] → single, parallel [62], single.
traverseSequential in @solana/instruction-plans awaits each child in turn, so the creating
transaction is confirmed before any write is attempted.
The plan and the executor are both correct. The gap is between sendAndConfirmTransaction
resolving at confirmed and the next transaction's estimation simulate being issued.
Where
createDefaultTransactionPlannerAndExecutor (src/cli.ts) builds the executor as:
await sendAndConfirmTransaction(transaction, { ...config, commitment: 'confirmed' });
and, earlier in the same pipe, estimateAndSetCULimit(m, config) where config is only
{ abortSignal }. Nothing carries the slot of the confirmed prerequisite into the subsequent
simulateTransaction, so the read is served at whatever slot the endpoint happens to be at.
grep -c minContextSlot returns 0 in 0.5.1 and in 0.8.0.
Against a single-node test validator there is one bank and nothing can be behind, which is why
this does not reproduce locally.
Reproduction
- Deploy any program with a large IDL (~370 KB) to devnet.
program-metadata --rpc <hosted devnet endpoint> --keypair <upgrade authority> write idl <program-id> idl.json
Fails at a chunk write, consistently, on a hosted endpoint. Succeeds on
solana-test-validator, including one started with devnet's exact feature set
(37 features deactivated to match solana feature status --url devnet).
Ruled out along the way: cluster feature differences; a mismatched on-chain program (dumped from
devnet and from the local clone, byte-identical); and a differing plan (--export from both,
decoded and compared instruction by instruction — identical).
Impact
The account is left allocated, rent-paid and partially written, and fetch on a truncated
metadata account returns bytes rather than an error — so a consumer building a coder from it gets a
partial instruction set instead of a failure. Each failed attempt strands the rent (~0.4 SOL for a
372 KB IDL). There is also no close-buffer exposed through anchor idl, so the accounts are easy
to accumulate without noticing.
Suggested fix
Thread the confirmed transaction's slot through as minContextSlot on the dependent
simulateTransaction (and any dependent account reads) — the parameter exists for exactly this.
Retrying a simulation that fails with a missing-account error would also work, and would cover
getLatestBlockhash skew too.
A --concurrency CLI flag would additionally be useful. Serialising the executor is what I am
currently using as a mitigation, and it works because it puts a full round trip between the confirm
and the next dependent read — but it is a mitigation, not a fix: it makes the read late rather than
correct, and a sufficiently lagged node would still lose.
Environment
@solana-program/program-metadata 0.5.1 (the version anchor-cli 1.1.2 pins) — the relevant
code is unchanged in 0.8.0
@solana/instruction-plans 6.10.0
- Solana CLI 4.1.1, Node 20.18.1
- devnet, hosted RPC endpoint
Happy to open a PR if the minContextSlot threading is the direction you'd want.
Summary
Uploading a large metadata value (a ~372 KB Anchor IDL, ~64 chunk transactions) fails reliably
against a hosted devnet RPC, and leaves a full-size, rent-paid, partially written account
behind. It succeeds every time against a single-node
solana-test-validator.The cause is that the compute-unit estimation simulates each chunk write without a
minContextSlot, so a dependent read can be served by a node that has not yet seen thetransaction that created the buffer.
Program logs from the failing simulation:
Two ComputeBudget instructions and then straight to the metadata program, with no System program
invoke — so this is not the transaction that creates the buffer. It is a chunk
Write, failingbecause the buffer is not visible to the node serving the simulation.
loadedAccountsDataSizeis48 247, which is the metadata program alone; the 60 KB buffer was not loaded.
The ordering is not the problem
Worth stating, because it is the obvious first guess and it is wrong. Both of these were checked:
getCreateBufferInstructionPlanbuildssequentialInstructionPlan([CreateAccount, Allocate, SetAuthority, parallelInstructionPlan([writes])]),and the planner preserves it. Dumping the resulting transaction plan gives
sequential [3] → single, parallel [62], single.traverseSequentialin@solana/instruction-plansawaits each child in turn, so the creatingtransaction is confirmed before any write is attempted.
The plan and the executor are both correct. The gap is between
sendAndConfirmTransactionresolving at
confirmedand the next transaction's estimation simulate being issued.Where
createDefaultTransactionPlannerAndExecutor(src/cli.ts) builds the executor as:and, earlier in the same pipe,
estimateAndSetCULimit(m, config)whereconfigis only{ abortSignal }. Nothing carries the slot of the confirmed prerequisite into the subsequentsimulateTransaction, so the read is served at whatever slot the endpoint happens to be at.grep -c minContextSlotreturns 0 in 0.5.1 and in 0.8.0.Against a single-node test validator there is one bank and nothing can be behind, which is why
this does not reproduce locally.
Reproduction
program-metadata --rpc <hosted devnet endpoint> --keypair <upgrade authority> write idl <program-id> idl.jsonFails at a chunk write, consistently, on a hosted endpoint. Succeeds on
solana-test-validator, including one started with devnet's exact feature set(37 features deactivated to match
solana feature status --url devnet).Ruled out along the way: cluster feature differences; a mismatched on-chain program (dumped from
devnet and from the local clone, byte-identical); and a differing plan (
--exportfrom both,decoded and compared instruction by instruction — identical).
Impact
The account is left allocated, rent-paid and partially written, and
fetchon a truncatedmetadata account returns bytes rather than an error — so a consumer building a coder from it gets a
partial instruction set instead of a failure. Each failed attempt strands the rent (~0.4 SOL for a
372 KB IDL). There is also no
close-bufferexposed throughanchor idl, so the accounts are easyto accumulate without noticing.
Suggested fix
Thread the confirmed transaction's slot through as
minContextSloton the dependentsimulateTransaction(and any dependent account reads) — the parameter exists for exactly this.Retrying a simulation that fails with a missing-account error would also work, and would cover
getLatestBlockhashskew too.A
--concurrencyCLI flag would additionally be useful. Serialising the executor is what I amcurrently using as a mitigation, and it works because it puts a full round trip between the confirm
and the next dependent read — but it is a mitigation, not a fix: it makes the read late rather than
correct, and a sufficiently lagged node would still lose.
Environment
@solana-program/program-metadata0.5.1 (the versionanchor-cli1.1.2 pins) — the relevantcode is unchanged in 0.8.0
@solana/instruction-plans6.10.0Happy to open a PR if the
minContextSlotthreading is the direction you'd want.