Summary
Same shape of bug as #20809 / ethereum-bounty/erigon#7, but on the sender signature path rather than the EIP-7702 auth-tuple path — and it applies to any transaction type, not just SetCode.
A Transactions (0x02) or PooledTransactions (0x0a) devp2p packet containing one transaction with an unrecoverable sender signature (e.g. r=0) causes Erigon's parser to return ErrParseTxn. The batch loop in pool_txn_packets.go only swallows ErrRejected; on ErrParseTxn it aborts, dropping every sibling transaction that had already been parsed in the same packet, and fetch.go then kicks the peer.
A malicious peer can craft a cheap dummy tx with r=0, splice it into a packet alongside whatever sibling txs they want to suppress, and selectively censor those siblings while looking honest at the gossip layer.
Code
Sender recovery error path — pool_txn_parser.go:436-438:
addr, err := txn.Sender(*signer)
if err != nil {
return 0, fmt.Errorf("%w: recovering sender from signature: %s", ErrParseTxn, err) //nolint
}
Batch loops that only continue on ErrRejected — pool_txn_packets.go:181-187 and pool_txn_packets.go:212-218:
pos, err = ctx.ParseTransaction(payload, pos, txnSlots.Txns[i], …)
if err != nil {
if errors.Is(err, ErrRejected) {
txnSlots.Resize(uint(i))
i--
continue
}
return 0, err
}
Geth, by contrast, defers sender recovery (typically lazy / at admission), so a malformed sender signature on one tx does not poison its packet siblings.
Suggested fix
Mirror the EIP-7702 fix in #20809 for sender-sig recovery. Two reasonable options:
- Narrow: change the wrap from
ErrParseTxn to ErrRejected and return p (the position past the consumed tx) instead of 0, so ParseTransactions / ParsePooledTransactions66 skip the bad tx and continue with siblings. The position past the tx is already known by the time Step 8 runs. Bad tx is dropped, siblings survive, peer is not kicked.
- Proper: defer sender recovery to the txpool's
validateTx / admission stage — closer to Geth's design — so parsing is a pure structural transform that never touches signatures. More invasive.
I'd start with the narrow fix and consider (2) as a separate refactor.
Repro sketch
The same approach as the test added in #20809 (TestEIP7702BatchPoisoning), but with the middle tx being a plain DynamicFeeTransaction whose R is set to zero post-signing instead of a SetCode tx with a poisoned auth tuple. With the current code ParseTransactions returns ErrParseTxn and slots.Txns ends up populated only up to the bad tx; with a fix all three should survive.
Scope
Out of scope of #20809 because the bounty issue (ethereum-bounty/erigon#7) was scoped to EIP-7702. Filing this so the broader pattern doesn't get lost.
Summary
Same shape of bug as #20809 / ethereum-bounty/erigon#7, but on the sender signature path rather than the EIP-7702 auth-tuple path — and it applies to any transaction type, not just SetCode.
A
Transactions (0x02)orPooledTransactions (0x0a)devp2p packet containing one transaction with an unrecoverable sender signature (e.g.r=0) causes Erigon's parser to returnErrParseTxn. The batch loop inpool_txn_packets.goonly swallowsErrRejected; onErrParseTxnit aborts, dropping every sibling transaction that had already been parsed in the same packet, andfetch.gothen kicks the peer.A malicious peer can craft a cheap dummy tx with
r=0, splice it into a packet alongside whatever sibling txs they want to suppress, and selectively censor those siblings while looking honest at the gossip layer.Code
Sender recovery error path —
pool_txn_parser.go:436-438:Batch loops that only continue on
ErrRejected—pool_txn_packets.go:181-187andpool_txn_packets.go:212-218:Geth, by contrast, defers sender recovery (typically lazy / at admission), so a malformed sender signature on one tx does not poison its packet siblings.
Suggested fix
Mirror the EIP-7702 fix in #20809 for sender-sig recovery. Two reasonable options:
ErrParseTxntoErrRejectedand returnp(the position past the consumed tx) instead of0, soParseTransactions/ParsePooledTransactions66skip the bad tx and continue with siblings. The position past the tx is already known by the time Step 8 runs. Bad tx is dropped, siblings survive, peer is not kicked.validateTx/ admission stage — closer to Geth's design — so parsing is a pure structural transform that never touches signatures. More invasive.I'd start with the narrow fix and consider (2) as a separate refactor.
Repro sketch
The same approach as the test added in #20809 (
TestEIP7702BatchPoisoning), but with the middle tx being a plainDynamicFeeTransactionwhoseRis set to zero post-signing instead of a SetCode tx with a poisoned auth tuple. With the current codeParseTransactionsreturnsErrParseTxnandslots.Txnsends up populated only up to the bad tx; with a fix all three should survive.Scope
Out of scope of #20809 because the bounty issue (ethereum-bounty/erigon#7) was scoped to EIP-7702. Filing this so the broader pattern doesn't get lost.