Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions cmd/devp2p/internal/ethtest/suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ 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},
{Name: "DuplicatePooledTxs", Fn: s.TestDuplicatePooledTxs},
}
}

Expand Down Expand Up @@ -1187,3 +1189,41 @@ func (s *Suite) testBadBlobTx(t *utesting.T, tx *types.Transaction, badTx *types
t.Fatalf("%v", err)
}
}

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)
}

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 := 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.`)
s.testDuplicateTxs(t, s.sendDuplicatePooledTxsInOneMsg)
}
91 changes: 91 additions & 0 deletions cmd/devp2p/internal/ethtest/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,94 @@ 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
}

// 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 := &eth.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
}