fix(solana): report unsupported transaction versions instead of a bincode EOF - #424
Open
pepe-anchor wants to merge 2 commits into
Open
fix(solana): report unsupported transaction versions instead of a bincode EOF#424pepe-anchor wants to merge 2 commits into
pepe-anchor wants to merge 2 commits into
Conversation
…code EOF SIMD-0296 raises Solana's transaction size cap to 4096 bytes, but only for the new v1 format specced in SIMD-0385. We support legacy and v0 only, so the question was whether a v1 payload could slip through and get rendered under a wrong interpretation. It can't. v1's VersionByte is 129 and sits at offset 0 (v1 puts signatures last), whereas legacy/v0 start with a compact-u16 signature count where 0x81 decodes to >= 129 signatures, needing >= 8256 bytes of signature data. v1 caps at 4096, so it can never satisfy that, and valid v1 requires num_required_signatures >= 1 so there is no shape that gets through. What was wrong is the error. from_string discarded solana-sdk's diagnostic with `if let Ok(..)` and fell through to the legacy decoder, which had already misread the version prefix as a header byte, so callers got "Decode error: io error: unexpected end of file" for an unsupported version. Reject the v1 marker up front, and when both decoders fail, locate the message version byte to report the version we don't implement. No behaviour change for legacy or v0. Tests cover the v1 envelope across the SIMD-0385 caps (1-12 signatures, 2-64 addresses, up to the 4096-byte limit), an unknown versioned message prefix, and a guard that v0 still decodes as v0. Co-Authored-By: Claude <noreply@anthropic.com>
…ng byte Review of the previous commit found two defects, both from the same mistake: inferring transaction structure from raw bytes instead of letting the decoders decide. The v1 marker check ran before either decoder. Its comment justified treating a leading 0x81 as unambiguous by appealing to Solana's 1232-byte transaction cap, but that is a network rule this parser does not enforce (our limits are 10 MB CLI / 25 MB gRPC). A legacy transaction with 129 signatures encodes to a leading 0x81 via the outer compact-u16 signature count, decodes fine, and was being rejected as v1. That regressed legacy/v0 for a payload that previously worked. unsupported_message_version walked the bytes to locate the message version. decode_shortu16_len succeeds for any first byte below 0x80 whether or not the input is envelope-shaped, so random bytes could land on an offset whose byte had the high bit set and get reported as a specific unsupported version. A fix for misleading errors that emitted its own misleading error. Both decoders now run first. Only if both fail is the leading byte inspected, which is where the v1 marker can no longer shadow anything decodable. Otherwise we return the versioned decoder's error verbatim: solana-sdk already produces "invalid value: integer `N`, expected a valid transaction message version" and names version 127 as an off-chain message, so the byte-walking probe bought nothing and is deleted. Residual, accepted: garbage that starts with 0x81 and fails both decoders is still labeled v1. It is rejected either way, so this is diagnostic imprecision rather than a wrong accept, and far narrower than what it replaces. Tests: a 129-signature legacy transaction still decodes as legacy; version 2 and version 127 assert solana-sdk's diagnostics survive the wrapping; the v1 envelope sweep now traverses both decoders instead of short-circuiting at byte 0. Co-Authored-By: Claude <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR improves Solana transaction parsing diagnostics by preserving solana-sdk’s versioned-transaction decode errors and explicitly reporting SIMD-0385 v1 payloads as an unsupported version, avoiding misleading bincode EOF errors when unsupported/unknown message versions are encountered.
Changes:
- Update
SolanaTransactionWrapper::from_stringto (1) capture the versioned decode error, (2) attempt legacy decode, and (3) only then inspect the leading byte to specially label SIMD-0385v1(0x81) asUnsupportedVersion. - When neither decoder succeeds and the payload is not
v1, returnDecodeErrorcontaining the versioned decoder’s error verbatim (e.g., “expected a valid transaction message version”, “off-chain messages are not accepted”). - Add a focused test module covering
v1rejection, unknown version reporting, off-chain message reporting, and a regression case ensuring legacy transactions with 129 signatures (leading byte0x81) still decode.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
pepe-anchor
marked this pull request as ready for review
July 29, 2026 11:23
pepe-anchor
requested review from
nathan-margosian-anchor,
prasanna-anchorage and
shahan-khatchadourian-anchorage
July 29, 2026 11:24
| /// pin that behavior: we reject anything neither decoder can read, we say | ||
| /// why, and a legacy/v0 payload that merely happens to start with `0x81` | ||
| /// (an outer compact-u16 signature count of 129, 257, ...) still decodes. | ||
| mod unsupported_versions { |
There was a problem hiding this comment.
Are any of the functions in this mod unsupported_versions used anywhere?
nathan-margosian-anchor
approved these changes
Jul 30, 2026
nathan-margosian-anchor
left a comment
There was a problem hiding this comment.
Just a note that SIM-0296 does have a feature gate on Mainnet (even though the SIMD doc on Github doesn't have it)
solana feature status -um | grep 0296
6TkHkRmP7JZy1fdM6fg5uXn76wChQBWGokHBJzrLB3mj | inactive | NA | SIMD-0296: Raise CPI nesting limit from 4 to 8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
SIMD-0296 raises Solana's max transaction size from 1232 to 4096 bytes, but only for the new
v1format specced separately in SIMD-0385. Both arestatus: Reviewwith no feature key assigned. We support legacy and v0 only, so the question was whether av1payload could reach a user rendered under a wrong interpretation.It can't: a
v1envelope fails both of our decoders. But the error we returned for that (and for any unsupported message version) wasDecode error: io error: unexpected end of file, which points at nothing.from_stringdiscarded solana-sdk's precise diagnostic withif let Ok(..)and reported the legacy decoder's failure instead, after that decoder had already misread the version prefix as a header byte.What
Both decoders run first. Only if both fail do we inspect the payload, and then only the leading byte:
v1marker (0x80 | 1), returnUnsupportedVersionnaming SIMD-0385.DecodeErrorcarrying the versioned decoder's error verbatim. solana-sdk already emitsinvalid value: integerN, expected a valid transaction message versionfor an unknown version andoff-chain messages are not acceptedfor version 127, so there is nothing left to reconstruct.No behaviour change for legacy or v0: the only payloads whose outcome changes are ones that already failed.
Two defects this PR went through before landing here
Worth recording, because the first version of this change got the reasoning wrong and review caught it empirically.
Ordering. The
v1marker check originally ran before either decoder, justified by a comment claiming no legacy/v0 transaction could produce a leading0x81, on the grounds that doing so needs ≥129 signatures (≥8256 bytes) and Solana caps transactions at 1232. That cap is a network rule this parser does not enforce (our limits are 10 MB CLI / 25 MB gRPC). A 129-signature legacy transaction decodes fine here and was being rejected asv1, regressing a payload that previously worked. Running the check last removes the need for the argument entirely.Byte-walking version probe. A helper located the message version byte to name unsupported versions.
decode_shortu16_lensucceeds for any first byte below0x80regardless of whether the input is envelope-shaped, so random bytes could land on an offset whose byte had the high bit set and be reported as a specific unsupported version. A fix for misleading errors that produced its own. Deleted in favour of solana-sdk's error text.Accepted residual
Garbage that starts with
0x81and fails both decoders is still labeledv1(e.g. a truncated 129-signature legacy tx). It is rejected either way, so this is diagnostic imprecision, never a wrong accept, and much narrower than what it replaces.Out of scope
Implementing
v1. It is a new wire format (fixed-width counts, trailing signatures, no address lookup tables) and itsTransactionConfigMaskmoves priority fee / CU limit / heap into a header bitmask while ComputeBudget instructions become ignored-but-executed no-ops. That last part is a display trap for ourcompute_budgetpreset (av1tx could show a fee the validator ignores) and deserves its own change once SIMD-0385 is accepted.Rollback
Revert the two commits. The change is confined to
SolanaTransactionWrapper::from_stringplus its tests in one file; nothing persists state or changes a wire format.Backwards compatibility
Compatible. Legacy and v0 parse exactly as before, pinned by tests including the 129-signature case. The error variant for some already-failing payloads changes; nothing in the workspace matches on
TransactionParseErrorvariants, both stringify through the same gRPC error path.Test evidence
unsupported_versionsmodule incore/visualsign.rs:rejects_simd385_v1_with_an_unsupported_version_errorrejects_v1_across_the_whole_legal_envelope— sweeps the SIMD-0385 caps (1-12 signatures, 2-64 addresses, 0-3000 bytes instruction data, up to the 4096-byte limit); payloads traverse both decoders rather than short-circuiting at byte 0accepts_legacy_with_129_signatures_despite_leading_v1_marker_byte— the regression from the ordering defectreports_the_version_for_an_unknown_versioned_message_prefix— asserts solana-sdk's wording survivesnames_version_127_as_an_off_chain_messagestill_accepts_v0Reviewed at depth=high (quick-review + pr-review + deep-review) over three quality-loop iterations.
deep-reviewverified the ordering fix againstsolana-transaction/solana-messagesource and with a standalone probe crate built on the pinned dependency versions, confirmingbytes.first()is the outer short-vec signature-count byte and that returning the versioned error hides no better legacy-specific diagnostic.🤖 Generated with Claude Code