Skip to content

fix(rpc): close signed-read replay gap via decoder-level rejection#383

Open
samlaf wants to merge 1 commit intoveridise-audit-april-2026from
fix--rpc-signed-read-replay-as-write
Open

fix(rpc): close signed-read replay gap via decoder-level rejection#383
samlaf wants to merge 1 commit intoveridise-audit-april-2026from
fix--rpc-signed-read-replay-as-write

Conversation

@samlaf
Copy link
Copy Markdown
Contributor

@samlaf samlaf commented Apr 22, 2026

Depends on SeismicSystems/seismic-alloy#104.

Signed-read seismic transactions were previously rejected only at mempool admission. This might (?) work in a TEE world but is fragile, and given that we are planning to go to mainnet without TEEs it was a real issue. We might also one day want to enable external block building via builder API, which would bypass the mempool.

signed_read check is now done as part of 2718 decoding, which happens in:

  • p2p tx gossip (before pool admission).
  • eth_sendRawTransaction (before pool admission).
  • Block body RLP decode — both for locally-executed blocks and peer-received blocks, via Engine API or p2p.
  • Any future codepath that decodes RLP tx bytes.

eth_call uses a special purpose recover_raw_seismic_call_tx function which allows signed_read=true txs.

Side note

In send_raw_transaction, the TypedData arm now decodes the EIP-712 payload into a SeismicTxEnvelope, re-encodes as RLP, and delegates to EthTransactions::send_raw_transaction(bytes). This makes sure all ingestion paths go through the 2718 decoding function. Also added a TODO mentioning that this ingestion path is not needed, and we could update our clients to send via the Bytes path directly.

More generally, this is a first step in the right direction, but I think the even cleaner design is to enforce signed_reads cryptoraphically instead. See the "Future hard-fork requiring change to SeismicTx" section in SeismicSystems/seismic-alloy#104

Signed-read seismic transactions were previously rejected only at mempool
admission. Block producers ingesting txs through non-mempool paths —
builder API, private orderflow, or the EIP-712 `TypedData` variant of
`eth_sendRawTransaction` which bypassed `Decodable2718` — could include a
signed-read tx directly, letting an attacker who intercepted a signed
`eth_call` payload replay it as an actual state-changing transaction.

Consumes seismic-alloy's decoder-level rejection (#104, c1ce533) and
updates this crate accordingly:

* `send_raw_transaction`: the `TypedData` arm now decodes the EIP-712
  payload into a `SeismicTxEnvelope`, re-encodes as RLP, and delegates to
  `EthTransactions::send_raw_transaction(bytes)`. All signed-tx ingress
  now funnels through a single `Decodable2718::typed_decode` pipeline, so
  decode-time invariants (including the new signed-read rejection) apply
  uniformly. Removes the parallel pool-admission pipeline that was the
  bypass.

* For `eth_call`'s Bytes path, a new `recover_raw_seismic_call_tx` helper
  uses seismic-alloy's permissive `decode_2718_permit_seismic_calls` so
  legitimate signed-read payloads are still accepted.

* Removes the now-redundant mempool-level `validate_signed_read_for_write`
  check and its associated error variant.

Depends on seismic-alloy PR #104; the \`rev\` in Cargo.toml is temporary and
should be bumped to the merged-main commit before landing.
@samlaf samlaf requested a review from cdrappi as a code owner April 22, 2026 17:52
@github-actions
Copy link
Copy Markdown
Contributor

Based on my analysis of the diff and the commit message, I can now provide a comprehensive review.

Moves signed-read validation from mempool to decoder level to close replay attack vulnerability.

The changes look correct and address an important security issue where attackers could replay intercepted eth_call signed-read payloads as state-changing transactions by bypassing mempool validation through alternative ingress paths.

Phase 1
No critical issues found. The security fix properly:

  • Unifies all signed-tx ingress through Decodable2718 pipeline with decoder-level validation
  • Uses permissive decode_2718_permit_seismic_calls only for legitimate eth_call paths
  • Removes the bypassed mempool validation that was the security gap

Phase 2

  • Cargo.toml:787 — The TODO comment mentions this temporarily points to seismic-alloy PR Remove error in Reth logs #104. The dependency update should be finalized to the merged commit before landing this PR.
  • crates/seismic/txpool/src/validator.rs:107 — The TODO about moving recent_block_hash and expires_at_block validation to consensus level is important. These checks can currently be bypassed via builder API or other non-mempool paths, similar to the signed-read issue that was just fixed.

The refactoring successfully consolidates transaction handling by removing the parallel SeismicTransaction trait and send_typed_data_transaction method, while the security improvement ensures decode-time invariants apply uniformly across all transaction ingress paths.

@samlaf samlaf changed the base branch from seismic to veridise-audit-april-2026 April 30, 2026 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant