From cb697a4b3e4e83dbe154401935cec7e0564dab18 Mon Sep 17 00:00:00 2001 From: jsvisa Date: Mon, 20 Oct 2025 22:28:19 +0800 Subject: [PATCH 1/3] cmd/devp2p: add duplicated tx test --- cmd/devp2p/internal/ethtest/suite.go | 30 ++++++++++++++++++++++ cmd/devp2p/internal/ethtest/transaction.go | 28 ++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/cmd/devp2p/internal/ethtest/suite.go b/cmd/devp2p/internal/ethtest/suite.go index c23360bf821..80d9b6d8d9d 100644 --- a/cmd/devp2p/internal/ethtest/suite.go +++ b/cmd/devp2p/internal/ethtest/suite.go @@ -90,6 +90,7 @@ func (s *Suite) EthTests() []utesting.Test { {Name: "BlobViolations", Fn: s.TestBlobViolations}, {Name: "TestBlobTxWithoutSidecar", Fn: s.TestBlobTxWithoutSidecar}, {Name: "TestBlobTxWithMismatchedSidecar", Fn: s.TestBlobTxWithMismatchedSidecar}, + {Name: "DuplicateTxs", Fn: s.TestDuplicateTxs}, } } @@ -1187,3 +1188,32 @@ func (s *Suite) testBadBlobTx(t *utesting.T, tx *types.Transaction, badTx *types t.Fatalf("%v", err) } } + +func (s *Suite) TestDuplicateTxs(t *utesting.T) { + t.Log(`This test sends a TransactionsMsg containing duplicate transactions and expects the node to disconnect.`) + + // Nudge client out of syncing mode to accept pending txs. + if err := s.engine.sendForkchoiceUpdated(); err != nil { + t.Fatalf("failed to send next block: %v", err) + } + + from, nonce := s.chain.GetSender(0) + inner := &types.DynamicFeeTx{ + ChainID: s.chain.config.ChainID, + Nonce: nonce, + GasTipCap: common.Big1, + GasFeeCap: s.chain.Head().BaseFee(), + Gas: 30000, + To: &common.Address{0xaa}, + Value: common.Big1, + } + tx, err := s.chain.SignTx(from, types.NewTx(inner)) + if err != nil { + t.Fatalf("failed to sign tx: %v", err) + } + + txs := []*types.Transaction{tx, tx} + if err := s.sendDuplicateTxsInOneMsg(t, txs); err != nil { + t.Fatal(err) + } +} diff --git a/cmd/devp2p/internal/ethtest/transaction.go b/cmd/devp2p/internal/ethtest/transaction.go index cbbbbce8d94..86aff3f7b07 100644 --- a/cmd/devp2p/internal/ethtest/transaction.go +++ b/cmd/devp2p/internal/ethtest/transaction.go @@ -176,3 +176,31 @@ func (s *Suite) sendInvalidTxs(t *utesting.T, txs []*types.Transaction) error { } } } + +// sendDuplicateTxsInOneMsg sends a transaction slice containing duplicates +// in a single TransactionsMsg and expects the node to disconnect. +func (s *Suite) sendDuplicateTxsInOneMsg(t *utesting.T, txs []*types.Transaction) error { + conn, err := s.dial() + if err != nil { + return fmt.Errorf("dial failed: %v", err) + } + defer conn.Close() + + if err = conn.peer(s.chain, nil); err != nil { + return fmt.Errorf("peering failed: %v", err) + } + + if err = conn.Write(ethProto, eth.TransactionsMsg, eth.TransactionsPacket(txs)); err != nil { + return fmt.Errorf("failed to write message to connection: %v", err) + } + + // Expect disconnect. + code, _, err := conn.Read() + if err != nil { + return fmt.Errorf("error reading from connection: %v", err) + } + if code != discMsg { + return fmt.Errorf("expected disconnect, got msg code %d", code) + } + return nil +} From 107f54a05df09f52c19730dac37d590dff7a4b2b Mon Sep 17 00:00:00 2001 From: jsvisa Date: Mon, 20 Oct 2025 23:17:31 +0800 Subject: [PATCH 2/3] cmd/devp2p: add duplicated pooled txs --- cmd/devp2p/internal/ethtest/suite.go | 31 +++++++++++ cmd/devp2p/internal/ethtest/transaction.go | 63 ++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/cmd/devp2p/internal/ethtest/suite.go b/cmd/devp2p/internal/ethtest/suite.go index 80d9b6d8d9d..d41a67e8f62 100644 --- a/cmd/devp2p/internal/ethtest/suite.go +++ b/cmd/devp2p/internal/ethtest/suite.go @@ -91,6 +91,7 @@ func (s *Suite) EthTests() []utesting.Test { {Name: "TestBlobTxWithoutSidecar", Fn: s.TestBlobTxWithoutSidecar}, {Name: "TestBlobTxWithMismatchedSidecar", Fn: s.TestBlobTxWithMismatchedSidecar}, {Name: "DuplicateTxs", Fn: s.TestDuplicateTxs}, + {Name: "DuplicatePooledTxs", Fn: s.TestDuplicatePooledTxs}, } } @@ -1217,3 +1218,33 @@ func (s *Suite) TestDuplicateTxs(t *utesting.T) { t.Fatal(err) } } + +func (s *Suite) TestDuplicatePooledTxs(t *utesting.T) { + t.Log(`This test announces transaction hashes to the node, then sends a PooledTransactionsMsg containing duplicate transactions and expects the node to disconnect.`) + + // Nudge client out of syncing mode to accept pending txs. + if err := s.engine.sendForkchoiceUpdated(); err != nil { + t.Fatalf("failed to send next block: %v", err) + } + + from, nonce := s.chain.GetSender(1) + inner := &types.DynamicFeeTx{ + ChainID: s.chain.config.ChainID, + Nonce: nonce, + GasTipCap: common.Big1, + GasFeeCap: s.chain.Head().BaseFee(), + Gas: 75000, + To: &common.Address{0xbb}, + Value: common.Big1, + } + tx, err := s.chain.SignTx(from, types.NewTx(inner)) + if err != nil { + t.Fatalf("failed to sign tx: %v", err) + } + + // Send the same transaction twice in the PooledTransactionsMsg. + txs := []*types.Transaction{tx, tx} + if err := s.sendDuplicatePooledTxsInOneMsg(t, txs); err != nil { + t.Fatal(err) + } +} diff --git a/cmd/devp2p/internal/ethtest/transaction.go b/cmd/devp2p/internal/ethtest/transaction.go index 86aff3f7b07..367e97d9890 100644 --- a/cmd/devp2p/internal/ethtest/transaction.go +++ b/cmd/devp2p/internal/ethtest/transaction.go @@ -204,3 +204,66 @@ func (s *Suite) sendDuplicateTxsInOneMsg(t *utesting.T, txs []*types.Transaction } return nil } + +// sendDuplicatePooledTxsInOneMsg sends a PooledTransactionsMsg containing duplicates +// and expects the node to disconnect. +func (s *Suite) sendDuplicatePooledTxsInOneMsg(t *utesting.T, txs []*types.Transaction) error { + // First, announce the transactions so the node will request them. + announceConn, err := s.dial() + if err != nil { + return fmt.Errorf("dial failed: %v", err) + } + defer announceConn.Close() + + if err = announceConn.peer(s.chain, nil); err != nil { + return fmt.Errorf("peering failed: %v", err) + } + + // Create announcement for the transactions. + var ( + hashes = make([]common.Hash, len(txs)) + types = make([]byte, len(txs)) + sizes = make([]uint32, len(txs)) + ) + for i, tx := range txs { + hashes[i] = tx.Hash() + types[i] = tx.Type() + sizes[i] = uint32(tx.Size()) + } + + ann := eth.NewPooledTransactionHashesPacket{ + Types: types, + Sizes: sizes, + Hashes: hashes, + } + + if err := announceConn.Write(ethProto, eth.NewPooledTransactionHashesMsg, ann); err != nil { + return fmt.Errorf("failed to announce transactions: %v", err) + } + + // Wait for GetPooledTransactions request. + req := new(eth.GetPooledTransactionsPacket) + if err := announceConn.ReadMsg(ethProto, eth.GetPooledTransactionsMsg, req); err != nil { + return fmt.Errorf("failed to read GetPooledTransactions: %v", err) + } + + // Send response with duplicate transactions. + resp := ð.PooledTransactionsPacket{ + RequestId: req.RequestId, + PooledTransactionsResponse: eth.PooledTransactionsResponse(txs), + } + + if err := announceConn.Write(ethProto, eth.PooledTransactionsMsg, resp); err != nil { + return fmt.Errorf("failed to send PooledTransactions: %v", err) + } + + // Expect disconnect. + code, _, err := announceConn.Read() + if err != nil { + return fmt.Errorf("error reading from connection: %v", err) + } + if code != discMsg { + return fmt.Errorf("expected disconnect, got msg code %d", code) + } + return nil +} From 144ccf1c6022ca6e8e654253889e6bdcf44af9a5 Mon Sep 17 00:00:00 2001 From: jsvisa Date: Mon, 20 Oct 2025 23:58:40 +0800 Subject: [PATCH 3/3] internal: dry --- cmd/devp2p/internal/ethtest/suite.go | 39 +++++++--------------------- 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/cmd/devp2p/internal/ethtest/suite.go b/cmd/devp2p/internal/ethtest/suite.go index d41a67e8f62..d55087a1fe2 100644 --- a/cmd/devp2p/internal/ethtest/suite.go +++ b/cmd/devp2p/internal/ethtest/suite.go @@ -1190,9 +1190,7 @@ func (s *Suite) testBadBlobTx(t *utesting.T, tx *types.Transaction, badTx *types } } -func (s *Suite) TestDuplicateTxs(t *utesting.T) { - t.Log(`This test sends a TransactionsMsg containing duplicate transactions and expects the node to disconnect.`) - +func (s *Suite) testDuplicateTxs(t *utesting.T, sendFunc func(*utesting.T, []*types.Transaction) error) { // Nudge client out of syncing mode to accept pending txs. if err := s.engine.sendForkchoiceUpdated(); err != nil { t.Fatalf("failed to send next block: %v", err) @@ -1214,37 +1212,18 @@ func (s *Suite) TestDuplicateTxs(t *utesting.T) { } txs := []*types.Transaction{tx, tx} - if err := s.sendDuplicateTxsInOneMsg(t, txs); err != nil { + if err := sendFunc(t, txs); err != nil { t.Fatal(err) } + s.chain.IncNonce(from, 1) +} + +func (s *Suite) TestDuplicateTxs(t *utesting.T) { + t.Log(`This test sends a TransactionsMsg containing duplicate transactions and expects the node to disconnect.`) + s.testDuplicateTxs(t, s.sendDuplicateTxsInOneMsg) } func (s *Suite) TestDuplicatePooledTxs(t *utesting.T) { t.Log(`This test announces transaction hashes to the node, then sends a PooledTransactionsMsg containing duplicate transactions and expects the node to disconnect.`) - - // Nudge client out of syncing mode to accept pending txs. - if err := s.engine.sendForkchoiceUpdated(); err != nil { - t.Fatalf("failed to send next block: %v", err) - } - - from, nonce := s.chain.GetSender(1) - inner := &types.DynamicFeeTx{ - ChainID: s.chain.config.ChainID, - Nonce: nonce, - GasTipCap: common.Big1, - GasFeeCap: s.chain.Head().BaseFee(), - Gas: 75000, - To: &common.Address{0xbb}, - Value: common.Big1, - } - tx, err := s.chain.SignTx(from, types.NewTx(inner)) - if err != nil { - t.Fatalf("failed to sign tx: %v", err) - } - - // Send the same transaction twice in the PooledTransactionsMsg. - txs := []*types.Transaction{tx, tx} - if err := s.sendDuplicatePooledTxsInOneMsg(t, txs); err != nil { - t.Fatal(err) - } + s.testDuplicateTxs(t, s.sendDuplicatePooledTxsInOneMsg) }