Skip to content

fix(solana): report unsupported transaction versions instead of a bincode EOF - #424

Open
pepe-anchor wants to merge 2 commits into
mainfrom
fix/solana-reject-unsupported-tx-version
Open

fix(solana): report unsupported transaction versions instead of a bincode EOF#424
pepe-anchor wants to merge 2 commits into
mainfrom
fix/solana-reject-unsupported-tx-version

Conversation

@pepe-anchor

@pepe-anchor pepe-anchor commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Why

SIMD-0296 raises Solana's max transaction size from 1232 to 4096 bytes, but only for the new v1 format specced separately in SIMD-0385. Both are status: Review with no feature key assigned. We support legacy and v0 only, so the question was whether a v1 payload could reach a user rendered under a wrong interpretation.

It can't: a v1 envelope fails both of our decoders. But the error we returned for that (and for any unsupported message version) was Decode error: io error: unexpected end of file, which points at nothing. from_string discarded solana-sdk's precise diagnostic with if 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:

  • If it is the v1 marker (0x80 | 1), return UnsupportedVersion naming SIMD-0385.
  • Otherwise return DecodeError carrying the versioned decoder's error verbatim. solana-sdk already emits invalid value: integer N, expected a valid transaction message version for an unknown version and off-chain messages are not accepted for 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 v1 marker check originally ran before either decoder, justified by a comment claiming no legacy/v0 transaction could produce a leading 0x81, 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 as v1, 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_len succeeds for any first byte below 0x80 regardless 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 0x81 and fails both decoders is still labeled v1 (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 its TransactionConfigMask moves 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 our compute_budget preset (a v1 tx 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_string plus 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 TransactionParseError variants, both stringify through the same gRPC error path.

Test evidence

unsupported_versions module in core/visualsign.rs:

  • rejects_simd385_v1_with_an_unsupported_version_error
  • rejects_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 0
  • accepts_legacy_with_129_signatures_despite_leading_v1_marker_byte — the regression from the ordering defect
  • reports_the_version_for_an_unknown_versioned_message_prefix — asserts solana-sdk's wording survives
  • names_version_127_as_an_off_chain_message
  • still_accepts_v0
cargo test -p visualsign-solana            # 328 passed, 0 failed (9 binaries)
cargo clippy --all-targets -- -D warnings  # exit 0, workspace-wide
cargo fmt --check                          # clean

Reviewed at depth=high (quick-review + pr-review + deep-review) over three quality-loop iterations. deep-review verified the ordering fix against solana-transaction/solana-message source and with a standalone probe crate built on the pinned dependency versions, confirming bytes.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

…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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_string to (1) capture the versioned decode error, (2) attempt legacy decode, and (3) only then inspect the leading byte to specially label SIMD-0385 v1 (0x81) as UnsupportedVersion.
  • When neither decoder succeeds and the payload is not v1, return DecodeError containing 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 v1 rejection, unknown version reporting, off-chain message reporting, and a regression case ensuring legacy transactions with 129 signatures (leading byte 0x81) still decode.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

/// 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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are any of the functions in this mod unsupported_versions used anywhere?

@nathan-margosian-anchor nathan-margosian-anchor left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants