diff --git a/cmd/devp2p/internal/ethtest/suite.go b/cmd/devp2p/internal/ethtest/suite.go index c23360bf82..80d9b6d8d9 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 cbbbbce8d9..86aff3f7b0 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 +}