Skip to content

Commit f6ca633

Browse files
MichaelRiabzev-StarkWareMichaelRiabzevkaralabe
authored andcommitted
core: count tx size in slots, bump max size ot 4x32KB (ethereum#20352)
* tests for tx size * alow multiple slots transactions * tests for tx size limit (32 KB) * change tx size tests to use addRemoteSync instead of validateTx (requested in pool request). * core: minor tx slotting polishes, add slot tracking metric Co-authored-by: Michael Riabzev <[email protected]> Co-authored-by: Péter Szilágyi <[email protected]>
1 parent 063da3b commit f6ca633

File tree

3 files changed

+132
-17
lines changed

3 files changed

+132
-17
lines changed

core/tx_list.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -494,11 +494,11 @@ func (l *txPricedList) Underpriced(tx *types.Transaction, local *accountSet) boo
494494

495495
// Discard finds a number of most underpriced transactions, removes them from the
496496
// priced list and returns them for further removal from the entire pool.
497-
func (l *txPricedList) Discard(count int, local *accountSet) types.Transactions {
498-
drop := make(types.Transactions, 0, count) // Remote underpriced transactions to drop
497+
func (l *txPricedList) Discard(slots int, local *accountSet) types.Transactions {
498+
drop := make(types.Transactions, 0, slots) // Remote underpriced transactions to drop
499499
save := make(types.Transactions, 0, 64) // Local underpriced transactions to keep
500500

501-
for len(*l.items) > 0 && count > 0 {
501+
for len(*l.items) > 0 && slots > 0 {
502502
// Discard stale transactions if found during cleanup
503503
tx := heap.Pop(l.items).(*types.Transaction)
504504
if l.all.Get(tx.Hash()) == nil {
@@ -510,7 +510,7 @@ func (l *txPricedList) Discard(count int, local *accountSet) types.Transactions
510510
save = append(save, tx)
511511
} else {
512512
drop = append(drop, tx)
513-
count--
513+
slots -= numSlots(tx)
514514
}
515515
}
516516
for _, tx := range save {

core/tx_pool.go

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ import (
3838
const (
3939
// chainHeadChanSize is the size of channel listening to ChainHeadEvent.
4040
chainHeadChanSize = 10
41+
42+
// txSlotSize is used to calculate how many data slots a single transaction
43+
// takes up based on its size. The slots are used as DoS protection, ensuring
44+
// that validating a new transaction remains a constant operation (in reality
45+
// O(maxslots), where max slots are 4 currently).
46+
txSlotSize = 32 * 1024
47+
48+
// txMaxSize is the maximum size a single transaction can have. This field has
49+
// non-trivial consequences: larger transactions are significantly harder and
50+
// more expensive to propagate; larger transactions also take more resources
51+
// to validate whether they fit into the pool or not.
52+
txMaxSize = 4 * txSlotSize // 128KB, don't bump without chunking support
4153
)
4254

4355
var (
@@ -105,6 +117,7 @@ var (
105117
pendingGauge = metrics.NewRegisteredGauge("txpool/pending", nil)
106118
queuedGauge = metrics.NewRegisteredGauge("txpool/queued", nil)
107119
localGauge = metrics.NewRegisteredGauge("txpool/local", nil)
120+
slotsGauge = metrics.NewRegisteredGauge("txpool/slots", nil)
108121
)
109122

110123
// TxStatus is the current status of a transaction as seen by the pool.
@@ -510,8 +523,8 @@ func (pool *TxPool) local() map[common.Address]types.Transactions {
510523
// validateTx checks whether a transaction is valid according to the consensus
511524
// rules and adheres to some heuristic limits of the local node (price and size).
512525
func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
513-
// Heuristic limit, reject transactions over 32KB to prevent DOS attacks
514-
if tx.Size() > 32*1024 {
526+
// Reject transactions over defined size to prevent DOS attacks
527+
if uint64(tx.Size()) > txMaxSize {
515528
return ErrOversizedData
516529
}
517530
// Transactions can't be negative. This may never happen using RLP decoded
@@ -583,7 +596,7 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e
583596
return false, ErrUnderpriced
584597
}
585598
// New transaction is better than our worse ones, make room for it
586-
drop := pool.priced.Discard(pool.all.Count()-int(pool.config.GlobalSlots+pool.config.GlobalQueue-1), pool.locals)
599+
drop := pool.priced.Discard(pool.all.Slots()-int(pool.config.GlobalSlots+pool.config.GlobalQueue)+numSlots(tx), pool.locals)
587600
for _, tx := range drop {
588601
log.Trace("Discarding freshly underpriced transaction", "hash", tx.Hash(), "price", tx.GasPrice())
589602
underpricedTxMeter.Mark(1)
@@ -1493,8 +1506,9 @@ func (as *accountSet) merge(other *accountSet) {
14931506
// peeking into the pool in TxPool.Get without having to acquire the widely scoped
14941507
// TxPool.mu mutex.
14951508
type txLookup struct {
1496-
all map[common.Hash]*types.Transaction
1497-
lock sync.RWMutex
1509+
all map[common.Hash]*types.Transaction
1510+
slots int
1511+
lock sync.RWMutex
14981512
}
14991513

15001514
// newTxLookup returns a new txLookup structure.
@@ -1532,11 +1546,22 @@ func (t *txLookup) Count() int {
15321546
return len(t.all)
15331547
}
15341548

1549+
// Slots returns the current number of slots used in the lookup.
1550+
func (t *txLookup) Slots() int {
1551+
t.lock.RLock()
1552+
defer t.lock.RUnlock()
1553+
1554+
return t.slots
1555+
}
1556+
15351557
// Add adds a transaction to the lookup.
15361558
func (t *txLookup) Add(tx *types.Transaction) {
15371559
t.lock.Lock()
15381560
defer t.lock.Unlock()
15391561

1562+
t.slots += numSlots(tx)
1563+
slotsGauge.Update(int64(t.slots))
1564+
15401565
t.all[tx.Hash()] = tx
15411566
}
15421567

@@ -1545,5 +1570,13 @@ func (t *txLookup) Remove(hash common.Hash) {
15451570
t.lock.Lock()
15461571
defer t.lock.Unlock()
15471572

1573+
t.slots -= numSlots(t.all[hash])
1574+
slotsGauge.Update(int64(t.slots))
1575+
15481576
delete(t.all, hash)
15491577
}
1578+
1579+
// numSlots calculates the number of slots needed for a single transaction.
1580+
func numSlots(tx *types.Transaction) int {
1581+
return int((tx.Size() + txSlotSize - 1) / txSlotSize)
1582+
}

core/tx_pool_test.go

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,17 @@ func pricedTransaction(nonce uint64, gaslimit uint64, gasprice *big.Int, key *ec
7777
return tx
7878
}
7979

80+
func pricedDataTransaction(nonce uint64, gaslimit uint64, gasprice *big.Int, key *ecdsa.PrivateKey, bytes uint64) *types.Transaction {
81+
data := make([]byte, bytes)
82+
rand.Read(data)
83+
84+
tx, _ := types.SignTx(types.NewTransaction(nonce, common.Address{}, big.NewInt(0), gaslimit, gasprice, data), types.HomesteadSigner{}, key)
85+
return tx
86+
}
87+
8088
func setupTxPool() (*TxPool, *ecdsa.PrivateKey) {
8189
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()))
82-
blockchain := &testBlockChain{statedb, 1000000, new(event.Feed)}
90+
blockchain := &testBlockChain{statedb, 10000000, new(event.Feed)}
8391

8492
key, _ := crypto.GenerateKey()
8593
pool := NewTxPool(testTxPoolConfig, params.TestChainConfig, blockchain)
@@ -465,7 +473,7 @@ func TestTransactionDropping(t *testing.T) {
465473
pool, key := setupTxPool()
466474
defer pool.Stop()
467475

468-
account, _ := deriveSender(transaction(0, 0, key))
476+
account := crypto.PubkeyToAddress(key.PublicKey)
469477
pool.currentState.AddBalance(account, big.NewInt(1000))
470478

471479
// Add some pending and some queued transactions
@@ -674,7 +682,7 @@ func TestTransactionGapFilling(t *testing.T) {
674682
pool, key := setupTxPool()
675683
defer pool.Stop()
676684

677-
account, _ := deriveSender(transaction(0, 0, key))
685+
account := crypto.PubkeyToAddress(key.PublicKey)
678686
pool.currentState.AddBalance(account, big.NewInt(1000000))
679687

680688
// Keep track of transaction events to ensure all executables get announced
@@ -728,7 +736,7 @@ func TestTransactionQueueAccountLimiting(t *testing.T) {
728736
pool, key := setupTxPool()
729737
defer pool.Stop()
730738

731-
account, _ := deriveSender(transaction(0, 0, key))
739+
account := crypto.PubkeyToAddress(key.PublicKey)
732740
pool.currentState.AddBalance(account, big.NewInt(1000000))
733741

734742
// Keep queuing up transactions and make sure all above a limit are dropped
@@ -923,7 +931,7 @@ func TestTransactionPendingLimiting(t *testing.T) {
923931
pool, key := setupTxPool()
924932
defer pool.Stop()
925933

926-
account, _ := deriveSender(transaction(0, 0, key))
934+
account := crypto.PubkeyToAddress(key.PublicKey)
927935
pool.currentState.AddBalance(account, big.NewInt(1000000))
928936

929937
// Keep track of transaction events to ensure all executables get announced
@@ -1002,6 +1010,62 @@ func TestTransactionPendingGlobalLimiting(t *testing.T) {
10021010
}
10031011
}
10041012

1013+
// Test the limit on transaction size is enforced correctly.
1014+
// This test verifies every transaction having allowed size
1015+
// is added to the pool, and longer transactions are rejected.
1016+
func TestTransactionAllowedTxSize(t *testing.T) {
1017+
t.Parallel()
1018+
1019+
// Create a test account and fund it
1020+
pool, key := setupTxPool()
1021+
defer pool.Stop()
1022+
1023+
account := crypto.PubkeyToAddress(key.PublicKey)
1024+
pool.currentState.AddBalance(account, big.NewInt(1000000000))
1025+
1026+
// Compute maximal data size for transactions (lower bound).
1027+
//
1028+
// It is assumed the fields in the transaction (except of the data) are:
1029+
// - nonce <= 32 bytes
1030+
// - gasPrice <= 32 bytes
1031+
// - gasLimit <= 32 bytes
1032+
// - recipient == 20 bytes
1033+
// - value <= 32 bytes
1034+
// - signature == 65 bytes
1035+
// All those fields are summed up to at most 213 bytes.
1036+
baseSize := uint64(213)
1037+
dataSize := txMaxSize - baseSize
1038+
1039+
// Try adding a transaction with maximal allowed size
1040+
tx := pricedDataTransaction(0, pool.currentMaxGas, big.NewInt(1), key, dataSize)
1041+
if err := pool.addRemoteSync(tx); err != nil {
1042+
t.Fatalf("failed to add transaction of size %d, close to maximal: %v", int(tx.Size()), err)
1043+
}
1044+
// Try adding a transaction with random allowed size
1045+
if err := pool.addRemoteSync(pricedDataTransaction(1, pool.currentMaxGas, big.NewInt(1), key, uint64(rand.Intn(int(dataSize))))); err != nil {
1046+
t.Fatalf("failed to add transaction of random allowed size: %v", err)
1047+
}
1048+
// Try adding a transaction of minimal not allowed size
1049+
if err := pool.addRemoteSync(pricedDataTransaction(2, pool.currentMaxGas, big.NewInt(1), key, txMaxSize)); err == nil {
1050+
t.Fatalf("expected rejection on slightly oversize transaction")
1051+
}
1052+
// Try adding a transaction of random not allowed size
1053+
if err := pool.addRemoteSync(pricedDataTransaction(2, pool.currentMaxGas, big.NewInt(1), key, dataSize+1+uint64(rand.Intn(int(10*txMaxSize))))); err == nil {
1054+
t.Fatalf("expected rejection on oversize transaction")
1055+
}
1056+
// Run some sanity checks on the pool internals
1057+
pending, queued := pool.Stats()
1058+
if pending != 2 {
1059+
t.Fatalf("pending transactions mismatched: have %d, want %d", pending, 2)
1060+
}
1061+
if queued != 0 {
1062+
t.Fatalf("queued transactions mismatched: have %d, want %d", queued, 0)
1063+
}
1064+
if err := validateTxPoolInternals(pool); err != nil {
1065+
t.Fatalf("pool internal state corrupted: %v", err)
1066+
}
1067+
}
1068+
10051069
// Tests that if transactions start being capped, transactions are also removed from 'all'
10061070
func TestTransactionCapClearsFromAll(t *testing.T) {
10071071
t.Parallel()
@@ -1752,6 +1816,24 @@ func TestTransactionStatusCheck(t *testing.T) {
17521816
}
17531817
}
17541818

1819+
// Test the transaction slots consumption is computed correctly
1820+
func TestTransactionSlotCount(t *testing.T) {
1821+
t.Parallel()
1822+
1823+
key, _ := crypto.GenerateKey()
1824+
1825+
// Check that an empty transaction consumes a single slot
1826+
smallTx := pricedDataTransaction(0, 0, big.NewInt(0), key, 0)
1827+
if slots := numSlots(smallTx); slots != 1 {
1828+
t.Fatalf("small transactions slot count mismatch: have %d want %d", slots, 1)
1829+
}
1830+
// Check that a large transaction consumes the correct number of slots
1831+
bigTx := pricedDataTransaction(0, 0, big.NewInt(0), key, uint64(10*txSlotSize))
1832+
if slots := numSlots(bigTx); slots != 11 {
1833+
t.Fatalf("big transactions slot count mismatch: have %d want %d", slots, 11)
1834+
}
1835+
}
1836+
17551837
// Benchmarks the speed of validating the contents of the pending queue of the
17561838
// transaction pool.
17571839
func BenchmarkPendingDemotion100(b *testing.B) { benchmarkPendingDemotion(b, 100) }
@@ -1763,7 +1845,7 @@ func benchmarkPendingDemotion(b *testing.B, size int) {
17631845
pool, key := setupTxPool()
17641846
defer pool.Stop()
17651847

1766-
account, _ := deriveSender(transaction(0, 0, key))
1848+
account := crypto.PubkeyToAddress(key.PublicKey)
17671849
pool.currentState.AddBalance(account, big.NewInt(1000000))
17681850

17691851
for i := 0; i < size; i++ {
@@ -1788,7 +1870,7 @@ func benchmarkFuturePromotion(b *testing.B, size int) {
17881870
pool, key := setupTxPool()
17891871
defer pool.Stop()
17901872

1791-
account, _ := deriveSender(transaction(0, 0, key))
1873+
account := crypto.PubkeyToAddress(key.PublicKey)
17921874
pool.currentState.AddBalance(account, big.NewInt(1000000))
17931875

17941876
for i := 0; i < size; i++ {
@@ -1812,7 +1894,7 @@ func benchmarkPoolBatchInsert(b *testing.B, size int) {
18121894
pool, key := setupTxPool()
18131895
defer pool.Stop()
18141896

1815-
account, _ := deriveSender(transaction(0, 0, key))
1897+
account := crypto.PubkeyToAddress(key.PublicKey)
18161898
pool.currentState.AddBalance(account, big.NewInt(1000000))
18171899

18181900
batches := make([]types.Transactions, b.N)

0 commit comments

Comments
 (0)