Skip to content

Commit a00dc50

Browse files
authored
Merge pull request #21358 from hendrikhofstadt/fix/tx-sort-time
core: sort txs at the same gas price by received time
2 parents ff90894 + 298a19b commit a00dc50

File tree

2 files changed

+86
-22
lines changed

2 files changed

+86
-22
lines changed

core/types/transaction.go

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"io"
2323
"math/big"
2424
"sync/atomic"
25+
"time"
2526

2627
"github.com/ethereum/go-ethereum/common"
2728
"github.com/ethereum/go-ethereum/common/hexutil"
@@ -36,7 +37,9 @@ var (
3637
)
3738

3839
type Transaction struct {
39-
data txdata
40+
data txdata // Consensus contents of a transaction
41+
time time.Time // Time first seen locally (spam avoidance)
42+
4043
// caches
4144
hash atomic.Value
4245
size atomic.Value
@@ -100,8 +103,10 @@ func newTransaction(nonce uint64, to *common.Address, amount *big.Int, gasLimit
100103
if gasPrice != nil {
101104
d.Price.Set(gasPrice)
102105
}
103-
104-
return &Transaction{data: d}
106+
return &Transaction{
107+
data: d,
108+
time: time.Now(),
109+
}
105110
}
106111

107112
// ChainId returns which chain id this transaction was signed for (if at all)
@@ -134,8 +139,8 @@ func (tx *Transaction) DecodeRLP(s *rlp.Stream) error {
134139
err := s.Decode(&tx.data)
135140
if err == nil {
136141
tx.size.Store(common.StorageSize(rlp.ListSize(size)))
142+
tx.time = time.Now()
137143
}
138-
139144
return err
140145
}
141146

@@ -153,7 +158,6 @@ func (tx *Transaction) UnmarshalJSON(input []byte) error {
153158
if err := dec.UnmarshalJSON(input); err != nil {
154159
return err
155160
}
156-
157161
withSignature := dec.V.Sign() != 0 || dec.R.Sign() != 0 || dec.S.Sign() != 0
158162
if withSignature {
159163
var V byte
@@ -167,8 +171,10 @@ func (tx *Transaction) UnmarshalJSON(input []byte) error {
167171
return ErrInvalidSig
168172
}
169173
}
170-
171-
*tx = Transaction{data: dec}
174+
*tx = Transaction{
175+
data: dec,
176+
time: time.Now(),
177+
}
172178
return nil
173179
}
174180

@@ -246,7 +252,10 @@ func (tx *Transaction) WithSignature(signer Signer, sig []byte) (*Transaction, e
246252
if err != nil {
247253
return nil, err
248254
}
249-
cpy := &Transaction{data: tx.data}
255+
cpy := &Transaction{
256+
data: tx.data,
257+
time: tx.time,
258+
}
250259
cpy.data.R, cpy.data.S, cpy.data.V = r, s, v
251260
return cpy, nil
252261
}
@@ -306,19 +315,27 @@ func (s TxByNonce) Len() int { return len(s) }
306315
func (s TxByNonce) Less(i, j int) bool { return s[i].data.AccountNonce < s[j].data.AccountNonce }
307316
func (s TxByNonce) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
308317

309-
// TxByPrice implements both the sort and the heap interface, making it useful
318+
// TxByPriceAndTime implements both the sort and the heap interface, making it useful
310319
// for all at once sorting as well as individually adding and removing elements.
311-
type TxByPrice Transactions
312-
313-
func (s TxByPrice) Len() int { return len(s) }
314-
func (s TxByPrice) Less(i, j int) bool { return s[i].data.Price.Cmp(s[j].data.Price) > 0 }
315-
func (s TxByPrice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
320+
type TxByPriceAndTime Transactions
321+
322+
func (s TxByPriceAndTime) Len() int { return len(s) }
323+
func (s TxByPriceAndTime) Less(i, j int) bool {
324+
// If the prices are equal, use the time the transaction was first seen for
325+
// deterministic sorting
326+
cmp := s[i].data.Price.Cmp(s[j].data.Price)
327+
if cmp == 0 {
328+
return s[i].time.Before(s[j].time)
329+
}
330+
return cmp > 0
331+
}
332+
func (s TxByPriceAndTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
316333

317-
func (s *TxByPrice) Push(x interface{}) {
334+
func (s *TxByPriceAndTime) Push(x interface{}) {
318335
*s = append(*s, x.(*Transaction))
319336
}
320337

321-
func (s *TxByPrice) Pop() interface{} {
338+
func (s *TxByPriceAndTime) Pop() interface{} {
322339
old := *s
323340
n := len(old)
324341
x := old[n-1]
@@ -331,7 +348,7 @@ func (s *TxByPrice) Pop() interface{} {
331348
// entire batches of transactions for non-executable accounts.
332349
type TransactionsByPriceAndNonce struct {
333350
txs map[common.Address]Transactions // Per account nonce-sorted list of transactions
334-
heads TxByPrice // Next transaction for each unique account (price heap)
351+
heads TxByPriceAndTime // Next transaction for each unique account (price heap)
335352
signer Signer // Signer for the set of transactions
336353
}
337354

@@ -341,8 +358,8 @@ type TransactionsByPriceAndNonce struct {
341358
// Note, the input map is reowned so the caller should not interact any more with
342359
// if after providing it to the constructor.
343360
func NewTransactionsByPriceAndNonce(signer Signer, txs map[common.Address]Transactions) *TransactionsByPriceAndNonce {
344-
// Initialize a price based heap with the head transactions
345-
heads := make(TxByPrice, 0, len(txs))
361+
// Initialize a price and received time based heap with the head transactions
362+
heads := make(TxByPriceAndTime, 0, len(txs))
346363
for from, accTxs := range txs {
347364
heads = append(heads, accTxs[0])
348365
// Ensure the sender address is from the signer

core/types/transaction_test.go

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"encoding/json"
2323
"math/big"
2424
"testing"
25+
"time"
2526

2627
"github.com/ethereum/go-ethereum/common"
2728
"github.com/ethereum/go-ethereum/crypto"
@@ -127,8 +128,8 @@ func TestTransactionPriceNonceSort(t *testing.T) {
127128
for i := 0; i < len(keys); i++ {
128129
keys[i], _ = crypto.GenerateKey()
129130
}
130-
131131
signer := HomesteadSigner{}
132+
132133
// Generate a batch of transactions with overlapping values, but shifted nonces
133134
groups := map[common.Address]Transactions{}
134135
for start, key := range keys {
@@ -155,12 +156,10 @@ func TestTransactionPriceNonceSort(t *testing.T) {
155156
// Make sure the nonce order is valid
156157
for j, txj := range txs[i+1:] {
157158
fromj, _ := Sender(signer, txj)
158-
159159
if fromi == fromj && txi.Nonce() > txj.Nonce() {
160160
t.Errorf("invalid nonce ordering: tx #%d (A=%x N=%v) < tx #%d (A=%x N=%v)", i, fromi[:4], txi.Nonce(), i+j, fromj[:4], txj.Nonce())
161161
}
162162
}
163-
164163
// If the next tx has different from account, the price must be lower than the current one
165164
if i+1 < len(txs) {
166165
next := txs[i+1]
@@ -172,6 +171,54 @@ func TestTransactionPriceNonceSort(t *testing.T) {
172171
}
173172
}
174173

174+
// Tests that if multiple transactions have the same price, the ones seen earlier
175+
// are prioritized to avoid network spam attacks aiming for a specific ordering.
176+
func TestTransactionTimeSort(t *testing.T) {
177+
// Generate a batch of accounts to start with
178+
keys := make([]*ecdsa.PrivateKey, 5)
179+
for i := 0; i < len(keys); i++ {
180+
keys[i], _ = crypto.GenerateKey()
181+
}
182+
signer := HomesteadSigner{}
183+
184+
// Generate a batch of transactions with overlapping prices, but different creation times
185+
groups := map[common.Address]Transactions{}
186+
for start, key := range keys {
187+
addr := crypto.PubkeyToAddress(key.PublicKey)
188+
189+
tx, _ := SignTx(NewTransaction(0, common.Address{}, big.NewInt(100), 100, big.NewInt(1), nil), signer, key)
190+
tx.time = time.Unix(0, int64(len(keys)-start))
191+
192+
groups[addr] = append(groups[addr], tx)
193+
}
194+
// Sort the transactions and cross check the nonce ordering
195+
txset := NewTransactionsByPriceAndNonce(signer, groups)
196+
197+
txs := Transactions{}
198+
for tx := txset.Peek(); tx != nil; tx = txset.Peek() {
199+
txs = append(txs, tx)
200+
txset.Shift()
201+
}
202+
if len(txs) != len(keys) {
203+
t.Errorf("expected %d transactions, found %d", len(keys), len(txs))
204+
}
205+
for i, txi := range txs {
206+
fromi, _ := Sender(signer, txi)
207+
if i+1 < len(txs) {
208+
next := txs[i+1]
209+
fromNext, _ := Sender(signer, next)
210+
211+
if txi.GasPrice().Cmp(next.GasPrice()) < 0 {
212+
t.Errorf("invalid gasprice ordering: tx #%d (A=%x P=%v) < tx #%d (A=%x P=%v)", i, fromi[:4], txi.GasPrice(), i+1, fromNext[:4], next.GasPrice())
213+
}
214+
// Make sure time order is ascending if the txs have the same gas price
215+
if txi.GasPrice().Cmp(next.GasPrice()) == 0 && txi.time.After(next.time) {
216+
t.Errorf("invalid received time ordering: tx #%d (A=%x T=%v) > tx #%d (A=%x T=%v)", i, fromi[:4], txi.time, i+1, fromNext[:4], next.time)
217+
}
218+
}
219+
}
220+
}
221+
175222
// TestTransactionJSON tests serializing/de-serializing to/from JSON.
176223
func TestTransactionJSON(t *testing.T) {
177224
key, err := crypto.GenerateKey()

0 commit comments

Comments
 (0)