Skip to content

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

Closed
samlaf wants to merge 0 commit into
veridise-audit-april-2026from
fix--rpc-signed-read-replay-as-write
Closed

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

Conversation

@samlaf

@samlaf samlaf commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Fixes veridise-1083.

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

@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
@samlaf samlaf changed the title fix(rpc): close signed-read replay gap via decoder-level rejection fix(rpc): close signed-read replay gap via decoder-level rejection (Veridise-1083) Jun 17, 2026
@samlaf samlaf closed this Jun 17, 2026
@samlaf
samlaf force-pushed the fix--rpc-signed-read-replay-as-write branch from d9c5647 to b4a85e6 Compare June 17, 2026 14:57
@samlaf

samlaf commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Closed since this was included as part of #386 which was merged into the veridise-audit-april-2026 branch already.

samlaf added a commit that referenced this pull request Jun 24, 2026
…(Veridise 1083) (#422)

Fixes Veridise-1083.

Depends on SeismicSystems/seismic-alloy#104.

Signed-read seismic txs are an eth_call-only construct and must never be
executed as a state transition. PR #383
(#383) added a
decode-time rejection for this, but only on the pooled-type decoder
SeismicTxEnvelope::typed_decode — the RPC / mempool / tx-gossip path.

Block-ingestion paths decode via the consensus type
SeismicTransactionSigned::decode_2718 instead (engine newPayload, the
block executor's tx iterator, and p2p block bodies), which never goes
through the pooled decoder. So even with #383 merged, a block proposer
could still include a signed-read tx directly in a block and have every
node execute it as a state-changing tx — replaying an intercepted signed
eth_call payload as a write. This is the block-ingestion gap Veridise
1083 flagged.

Close it by adding the same rejection to the consensus decoder, gated on
signed_read alone (regardless of `to`). Such txs are now non-decodable
from the wire, so the Arbitrary impl clears the flag to keep generating
valid wire txs. Add regression tests for the reject/accept cases.
samlaf added a commit that referenced this pull request Jul 6, 2026
…(Veridise 1083) (#422)

Fixes Veridise-1083.

Depends on SeismicSystems/seismic-alloy#104.

Signed-read seismic txs are an eth_call-only construct and must never be
executed as a state transition. PR #383
(#383) added a
decode-time rejection for this, but only on the pooled-type decoder
SeismicTxEnvelope::typed_decode — the RPC / mempool / tx-gossip path.

Block-ingestion paths decode via the consensus type
SeismicTransactionSigned::decode_2718 instead (engine newPayload, the
block executor's tx iterator, and p2p block bodies), which never goes
through the pooled decoder. So even with #383 merged, a block proposer
could still include a signed-read tx directly in a block and have every
node execute it as a state-changing tx — replaying an intercepted signed
eth_call payload as a write. This is the block-ingestion gap Veridise
1083 flagged.

Close it by adding the same rejection to the consensus decoder, gated on
signed_read alone (regardless of `to`). Such txs are now non-decodable
from the wire, so the Arbitrary impl clears the flag to keep generating
valid wire txs. Add regression tests for the reject/accept cases.
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