Skip to content

Large uploads fail on hosted RPCs: chunk writes are simulated without minContextSlot #105

Description

@Vetted-Pro

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

  1. Deploy any program with a large IDL (~370 KB) to devnet.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions