Repository navigation
feat(solana): support V1 transactions and map the full Agave RPC error surface - #2336
Conversation
Bump maxSupportedTransactionVersion from 0 to 1 in the SOLANA GET_BLOCK_BY_NUM parse directive (mainnet-1 and testnet-2). V1 transactions are active on Devnet/Testnet and pending on Mainnet; nodes that do not yet know V1 accept the higher value unchanged. Claude-Session: https://claude.ai/code/session_01BmDq4Jk3Xh9EJhC9UdJoy3
Codecov Report✅ All modified and coverable lines are covered by tests.
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 1 file with indirect coverage changes 🚀 New features to boost your workflow:
|
Agave defines JSON-RPC server error codes -32001..-32021 in
rpc-client-api/src/custom_error.rs. Lava mapped 12 of them; the other 9
fell through to UNKNOWN_ERROR, which is retryable by default, so a
permanent node answer was retried across every provider in the pairing
before failing.
-32020 ("Transaction N not found" on getSignaturesForAddress before/until)
is new in Agave v4.0 and was the one actually reachable on mainnet today.
The rest are mapped alongside it so the gap does not reopen.
Eight map onto existing generic errors. -32017 gets a new Tier-2 code:
the epoch rewards window is chain state, identical on every provider, so
retrying it is always wasted work.
A coverage test now walks the whole -32001..-32021 block and fails on any
unmapped code, so a future Agave addition cannot silently regress to a
retryable UNKNOWN_ERROR.
Claude-Session: https://claude.ai/code/session_01BmDq4Jk3Xh9EJhC9UdJoy3
PR Summary by QodoSupport Solana V1 transactions and complete Agave RPC error mapping
AI Description
Diagram
High-Level Assessment
Files changed (6)
|
Code Review by Qodo
1. Version-one support can regress unseen
|
| {CodeEquals(-32017), LavaErrorChainSolanaEpochRewardsActive}, // "Epoch rewards period still active at slot N" | ||
| {CodeEquals(-32018), LavaErrorUserInvalidParams}, // "Rewards cannot be found; slot N is not epoch boundary" | ||
| {CodeEquals(-32019), LavaErrorNodeServiceUnavailable}, // "Failed to query long-term storage; please try again" | ||
| {CodeEquals(-32020), LavaErrorChainTxNotFound}, // "Transaction N not found" (getSignaturesForAddress before/until) |
There was a problem hiding this comment.
2. Missing transactions still get retried 🐞 Bug ≡ Correctness
The -32020 mapping uses LavaErrorChainTxNotFound, whose registry entry has Retryable: true. When getSignaturesForAddress reports that a before or until transaction is not found, retry handling therefore continues rotating providers despite the new coverage test's stated goal of avoiding retries for this deterministic response.
Agent Prompt
## Issue description
Agave error -32020 is mapped to the generic transaction-not-found error, which is retryable and therefore does not prevent the provider retries this change intends to eliminate.
## Fix Focus Areas
- protocol/common/error_classifier.go[43-43]
- protocol/common/error_codes.go[387-390]
- protocol/common/error_registry_test.go[1207-1214]
## Recommended Fix
Register a dedicated non-retryable Solana error for -32020 and map the code to it rather than changing the shared transaction-not-found error. Extend the tests to assert that -32020 is classified as that error and is non-retryable, rather than checking only that it is not unknown.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Every factual claim here is correct, but I'm keeping -32020 on CHAIN_TX_NOT_FOUND and keeping it retryable. Flagging clearly since this is a deliberate disagreement, not an oversight.
Why not the suggested fix. -32020 is not deterministic across providers. Whether the before/until signature is found depends on the node's ledger depth and whether long-term storage is enabled, so a pruned node returns -32020 where an archive node answers successfully. The message is byte-identical in both cases, so the classifier has no way to separate them. Confirmed live on both clusters with a well-formed signature that exists nowhere:
{"code":-32020,"message":"Transaction 5RkdZnHmvG95UqGHj9p5Vap8xEkbQCtBnkvLJYWfoD9Eu9azRhQmXTuzVFN1z21b385VdguUaYdunpZiBYGAZBB4 not found"}
Two further reasons. CHAIN_TX_NOT_FOUND (3202) and CHAIN_RECEIPT_NOT_FOUND (3203) are already retryable for exactly this ambiguity, so a non-retryable Solana variant would make this chain family inconsistent with every other one. And the costs are asymmetric: a wasted rotation costs latency, whereas suppressing the retry reports "not found" for data a different provider holds, which is a correctness regression and much harder to debug.
On the intent point, the comment being quoted describes why unmapped codes are dangerous, since UNKNOWN_ERROR retries by default. It was not a claim that -32020 must be non-retryable. That wording was genuinely misleading though, and 873c0f1 rewrites it: the problem with an unmapped code is that its retry behaviour is decided by the default rather than chosen.
Where this finding was right, and what changed. The tests asserted which error each code maps to but never whether it retries, so a reader could not tell deliberate from accidental. TestClassifyError_SolanaRetryPolicy now pins the retry decision for all 21 codes with the policy argument recorded beside each value. That matters more than it sounds: most targets are generic errors shared with other chain families, so flipping Retryable on one would have changed Solana's routing with every existing test still green. Verified by flipping CHAIN_TX_NOT_FOUND:
Solana -32020 (CHAIN_TX_NOT_FOUND) must have Retryable=true
— the before/until signature may exist on a provider with deeper history
The open question, for human reviewers. I could not empirically demonstrate the retryable branch, since I have no pruned Solana node to point at; that half rests on reading Agave's lookup path plus repo convention. It reduces to whether Lava's Solana provider fleet actually varies in history depth. If it is uniform, this finding is right and a non-retryable code is better. Note the Solana spec declares no archive extension, unlike ETH1, so Lava cannot route by history depth either way. Leaving this thread open rather than resolving it so that decision stays visible.
Generated by Claude Code
There was a problem hiding this comment.
Follow-up for anyone finding this thread after the merge.
This PR squash-merged as 04596ee before the test commit answering this finding reached its branch, so those tests missed the merge. They are now in #2338, which is tests only against the already-merged behaviour.
The -32020 decision is unchanged and the reasoning above still stands: it stays on CHAIN_TX_NOT_FOUND and stays retryable. TestClassifyError_SolanaRetryPolicy in #2338 pins that decision for all 21 codes, so it is now explicit rather than incidental, and flipping Retryable on a shared error fails loudly instead of silently changing Solana's provider rotation.
The open question travels to #2338 as well: whether Lava's Solana provider fleet actually varies in history depth. Uniform means this finding was right and a dedicated non-retryable code is the better call, which is a one-line change on top of that PR.
Generated by Claude Code
Follow-up to #2336, which merged before these tests were pushed. The behaviour is already on main; this only adds the guards for it, raised by the Qodo review on that PR. The spec bump had no test. TestSolanaSpec_GetBlockByNumSupportsV1Transactions is table-driven over both deployed copies of solana.json, asserting that GET_BLOCK_BY_NUM declares maxSupportedTransactionVersion 1 and that the directive is identical across networks. It decodes the rendered request rather than matching the template as a substring, so it survives key reordering and rejects the quoted "1" form that Solana does not accept. Nothing previously stopped the mainnet-1 and testnet-2 copies drifting. The classifier tests asserted which error each Agave code maps to but not whether that error retries. Most targets are generic errors shared with other chain families, so flipping Retryable on one of them would have changed Solana's provider rotation with every test still green. TestClassifyError_SolanaRetryPolicy pins the decision for all 21 codes and records the policy argument beside each value. -32020 stays on CHAIN_TX_NOT_FOUND and stays retryable, against the review's suggestion. The before/until signature may be absent from one node's history but present on a provider with a deeper ledger, and the error is byte-identical in both cases, so the classifier cannot distinguish them. A wasted rotation costs latency; refusing to retry would report "not found" for data another provider holds. This matches CHAIN_TX_NOT_FOUND and CHAIN_RECEIPT_NOT_FOUND on every other chain. Also corrects a comment that implied every code in the block should be non-retryable; the point is that an unmapped code has its retry behaviour decided by the default rather than chosen. Claude-Session: https://claude.ai/code/session_01BmDq4Jk3Xh9EJhC9UdJoy3
Description
Closes: N/A (Notion TSK-45 / Lava board "Solana Spec Upgrade – Support Upcoming V1 Transactions")
Three commits from one review pass over Solana's upgrade path. Everything is additive; nothing is removed or renamed.
feat(specs)— bumpmaxSupportedTransactionVersion0→1in the SOLANAGET_BLOCK_BY_NUMparse directive.feat(errors)— map the 9 Agave JSON-RPC error codes Lava was missing, including-32020which is new in Agave v4.0.test(solana)— pin the spec value and the per-code retry policy, in response to the Qodo review.Neither of the first two was prompted by an observed failure. Both come from Solana's published upgrade material and Agave's own source, checked against the repo.
1. V1 transaction support
Solana is activating V1 transactions (SIMD-0385): new wire format (discriminator byte
0x81), max size 4,096 bytes (was 1,232), no address-lookup tables. Active on Devnet and Testnet, pending on Mainnet.The bump lands in both
specs/mainnet-1/specs/solana.jsonandspecs/testnet-2/specs/solana.json(otherwise identical files).SOLANATandKOIIimportSOLANAand inherit it.Honest scope note: the directive uses
"transactionDetails":"none", and the node never applies the version check innone/signaturesmode, so the chain tracker would not have broken at0either. This is a correctness fix for the declared value, not a live-bug fix. It matters for any future directive that requests transaction details, and it removes the footgun before Mainnet activation.The rest of the spec was reviewed against the current RPC docs and found complete:
maxSupportedTransactionVersiongetConfirmedBlock,getRecentBlockhash,getFees, …) are absent from the spec.getBlock/getTransaction/blockSubscribemaxSupportedTransactionVersion; Lava does not inject or rewrite params.sendTransaction/simulateTransactionbase64.2. Agave RPC error-code coverage
Agave defines a contiguous block of JSON-RPC server error codes,
-32001through-32021, inrpc-client-api/src/custom_error.rs. Lava mapped 12. The other 9 fell through toUNKNOWN_ERROR, whose retry behaviour is the default rather than a choice — and the default is to retry, so a permanent, deterministic node answer was rotated across every provider in the pairing before failing.-32020is the one reachable on mainnet today: Agave v4.0 changedgetSignaturesForAddressto return it when thebefore/untilsignature is not found, instead of returning an empty array. The other 8 are mapped alongside it so the gap does not reopen.-32006CHAIN_INVALID_SIGNATURE(3016)-32008NODE_RESOURCE_UNAVAILABLE(2013)-32011CHAIN_DATA_NOT_AVAILABLE(3205)-32012NODE_INTERNAL_ERROR(2003)-32017CHAIN_SOLANA_EPOCH_REWARDS_ACTIVE(3312, new)-32018USER_INVALID_PARAMS(4003)-32019NODE_SERVICE_UNAVAILABLE(2006)-32020CHAIN_TX_NOT_FOUND(3202)-32021CHAIN_DATA_NOT_AVAILABLE(3205)Eight reuse existing generic errors, per the design doc's rule that a Tier-2 code is warranted only where retryability differs from the generic pattern.
-32017is the one new code. The epoch rewards distribution window is chain state — identical on every provider — so rotating during it is always wasted work. No existing error carries "data unavailable and non-retryable", hence code 3312.3. Tests added in response to review
Two real gaps, both now closed. Tests only; no production behaviour changed by this commit.
The spec bump had no test.
TestSolanaSpec_GetBlockByNumSupportsV1Transactionsis table-driven over both deployed copies. It decodes the rendered request rather than substring-matching the template, so it survives key reordering and rejects the quoted"1"form Solana does not accept, and it asserts the two networks' directives are byte-identical. Nothing previously stoppedmainnet-1andtestnet-2drifting.The classifier tests never asserted retryability. They pinned which error each code maps to, but most targets are generic errors shared with other chain families, so flipping
Retryableon one would have changed Solana's provider rotation with every test still green.TestClassifyError_SolanaRetryPolicynow pins the retry decision for all 21 codes with the policy argument recorded beside each value.-32020stays retryable, against the review's suggestion. Thebefore/untilsignature may be absent from one node's history but present on a provider with a deeper ledger or long-term storage enabled, and the error is byte-identical in both cases, so the classifier cannot distinguish them.CHAIN_TX_NOT_FOUNDandCHAIN_RECEIPT_NOT_FOUNDare already retryable for this same ambiguity on every other chain, and the costs are asymmetric: a wasted rotation costs latency, whereas suppressing the retry reports "not found" for data another provider holds. Reasoning and the one open question are on that review thread, left unresolved so the decision stays visible.Verification
Unit tests (all pass):
Every new test was mutation-tested — proven to fail, not just to pass. Each guard was broken deliberately, then restored:
specs/testnet-2reverted to0(half-applied edit)CHAIN_TX_NOT_FOUNDflipped to non-retryable-32021mapping deletedFor example:
Lava chain fetcher against live clusters (temporary test, not committed): built a jsonrpc chain parser + router + fetcher from the modified spec and ran
GET_BLOCKNUMthenGET_BLOCK_BY_NUMend to end.Mainnet accepts
maxSupportedTransactionVersion: 1(and higher) in the rawgetBlockcall today, so the bump is safe to propose before Mainnet activation.V1 transaction flows on Devnet (direct RPC, exercising what Lava passes through):
solders0.29: version 1, first byte0x81, 1,983 bytes.simulateTransactionwithencoding: base64:err: null, memo program executed.encoding: base58:-32602 "... too large: 2708 bytes (max: encoded/raw 1683/1232)", confirming base64 is mandatory above 1,232 bytes.-32020reproduced on both clusters with a well-formed signature that exists nowhere, confirming the error shape and message.Not covered here
getTransaction/getBlockat version0against a real V1 transaction. No V1 transaction was observed in the last 400 Devnet slots or the 2,000 most recent Memo/ComputeBudget signatures — only legacy and v0. Per the docsgetTransactionreturns-32015andgetBlockfails the whole call infull/accountsmode; Lava already maps-32015toCHAIN_SOLANA_TX_VERSION_UNSUPPORTED(non-retryable) and passes it through.-32020could not be demonstrated live, as no pruned Solana node was available to query. That argument rests on Agave's lookup path plus repo convention.scripts/pre_setups/init_solana_only_with_node.shagainst Devnet): not executed in the sandbox, which has noscreen/lavad.sendTransactionof a signed V1 tx: the Devnet faucet returned-32603 Internal erroron everyrequestAirdrop, so no funded key was available. Simulation covers the same encoding and size path.Author Checklist
All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.
I have...
!in the type prefix if API or client breaking change (not breaking — additive only)mainbrancherror-registry-design.mdtable; the spec JSON is its own documentation)Reviewers Checklist
All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.
I have...
https://claude.ai/code/session_01BmDq4Jk3Xh9EJhC9UdJoy3