Skip to content

Commit 300f1e2

Browse files
karalabeobscuren
authored andcommitted
[release/1.3.4] core, core/types, miner: fix transaction nonce-price combo sort
1 parent 4ce7970 commit 300f1e2

File tree

4 files changed

+131
-22
lines changed

4 files changed

+131
-22
lines changed

core/transaction_pool.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ func (self *TxPool) GetQueuedTransactions() types.Transactions {
365365
ret = append(ret, tx)
366366
}
367367
}
368-
sort.Sort(types.TxByNonce{ret})
368+
sort.Sort(types.TxByNonce(ret))
369369
return ret
370370
}
371371

core/types/transaction.go

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717
package types
1818

1919
import (
20+
"container/heap"
2021
"crypto/ecdsa"
2122
"errors"
2223
"fmt"
2324
"io"
2425
"math/big"
26+
"sort"
2527
"sync/atomic"
2628

2729
"github.com/ethereum/go-ethereum/common"
@@ -302,27 +304,78 @@ func TxDifference(a, b Transactions) (keep Transactions) {
302304
return keep
303305
}
304306

305-
type TxByNonce struct{ Transactions }
307+
// TxByNonce implements the sort interface to allow sorting a list of transactions
308+
// by their nonces. This is usually only useful for sorting transactions from a
309+
// single account, otherwise a nonce comparison doesn't make much sense.
310+
type TxByNonce Transactions
306311

307-
func (s TxByNonce) Less(i, j int) bool {
308-
return s.Transactions[i].data.AccountNonce < s.Transactions[j].data.AccountNonce
309-
}
312+
func (s TxByNonce) Len() int { return len(s) }
313+
func (s TxByNonce) Less(i, j int) bool { return s[i].data.AccountNonce < s[j].data.AccountNonce }
314+
func (s TxByNonce) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
315+
316+
// TxByPrice implements both the sort and the heap interface, making it useful
317+
// for all at once sorting as well as individually adding and removing elements.
318+
type TxByPrice Transactions
310319

311-
type TxByPrice struct{ Transactions }
320+
func (s TxByPrice) Len() int { return len(s) }
321+
func (s TxByPrice) Less(i, j int) bool { return s[i].data.Price.Cmp(s[j].data.Price) > 0 }
322+
func (s TxByPrice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
312323

313-
func (s TxByPrice) Less(i, j int) bool {
314-
return s.Transactions[i].data.Price.Cmp(s.Transactions[j].data.Price) > 0
324+
func (s *TxByPrice) Push(x interface{}) {
325+
*s = append(*s, x.(*Transaction))
315326
}
316327

317-
type TxByPriceAndNonce struct{ Transactions }
328+
func (s *TxByPrice) Pop() interface{} {
329+
old := *s
330+
n := len(old)
331+
x := old[n-1]
332+
*s = old[0 : n-1]
333+
return x
334+
}
318335

319-
func (s TxByPriceAndNonce) Less(i, j int) bool {
320-
// we can ignore the error here. Sorting shouldn't care about validness
321-
ifrom, _ := s.Transactions[i].From()
322-
jfrom, _ := s.Transactions[j].From()
323-
// favour nonce if they are from the same recipient
324-
if ifrom == jfrom {
325-
return s.Transactions[i].data.AccountNonce < s.Transactions[j].data.AccountNonce
336+
// SortByPriceAndNonce sorts the transactions by price in such a way that the
337+
// nonce orderings within a single account are maintained.
338+
//
339+
// Note, this is not as trivial as it seems from the first look as there are three
340+
// different criteria that need to be taken into account (price, nonce, account
341+
// match), which cannot be done with any plain sorting method, as certain items
342+
// cannot be compared without context.
343+
//
344+
// This method first sorts the separates the list of transactions into individual
345+
// sender accounts and sorts them by nonce. After the account nonce ordering is
346+
// satisfied, the results are merged back together by price, always comparing only
347+
// the head transaction from each account. This is done via a heap to keep it fast.
348+
func SortByPriceAndNonce(txs []*Transaction) {
349+
// Separate the transactions by account and sort by nonce
350+
byNonce := make(map[common.Address][]*Transaction)
351+
for _, tx := range txs {
352+
acc, _ := tx.From() // we only sort valid txs so this cannot fail
353+
byNonce[acc] = append(byNonce[acc], tx)
354+
}
355+
for _, accTxs := range byNonce {
356+
sort.Sort(TxByNonce(accTxs))
357+
}
358+
// Initialize a price based heap with the head transactions
359+
byPrice := make(TxByPrice, 0, len(byNonce))
360+
for acc, accTxs := range byNonce {
361+
byPrice = append(byPrice, accTxs[0])
362+
byNonce[acc] = accTxs[1:]
363+
}
364+
heap.Init(&byPrice)
365+
366+
// Merge by replacing the best with the next from the same account
367+
txs = txs[:0]
368+
for len(byPrice) > 0 {
369+
// Retrieve the next best transaction by price
370+
best := heap.Pop(&byPrice).(*Transaction)
371+
372+
// Push in its place the next transaction from the same account
373+
acc, _ := best.From() // we only sort valid txs so this cannot fail
374+
if accTxs, ok := byNonce[acc]; ok && len(accTxs) > 0 {
375+
heap.Push(&byPrice, accTxs[0])
376+
byNonce[acc] = accTxs[1:]
377+
}
378+
// Accumulate the best priced transaction
379+
txs = append(txs, best)
326380
}
327-
return s.Transactions[i].data.Price.Cmp(s.Transactions[j].data.Price) > 0
328381
}

core/types/transaction_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,60 @@ func TestRecipientNormal(t *testing.T) {
117117
t.Error("derived address doesn't match")
118118
}
119119
}
120+
121+
// Tests that transactions can be correctly sorted according to their price in
122+
// decreasing order, but at the same time with increasing nonces when issued by
123+
// the same account.
124+
func TestTransactionPriceNonceSort(t *testing.T) {
125+
// Generate a batch of accounts to start with
126+
keys := make([]*ecdsa.PrivateKey, 25)
127+
for i := 0; i < len(keys); i++ {
128+
keys[i], _ = crypto.GenerateKey()
129+
}
130+
// Generate a batch of transactions with overlapping values, but shifted nonces
131+
txs := []*Transaction{}
132+
for start, key := range keys {
133+
for i := 0; i < 25; i++ {
134+
tx, _ := NewTransaction(uint64(start+i), common.Address{}, big.NewInt(100), big.NewInt(100), big.NewInt(int64(start+i)), nil).SignECDSA(key)
135+
txs = append(txs, tx)
136+
}
137+
}
138+
// Sort the transactions and cross check the nonce ordering
139+
SortByPriceAndNonce(txs)
140+
for i, txi := range txs {
141+
fromi, _ := txi.From()
142+
143+
// Make sure the nonce order is valid
144+
for j, txj := range txs[i+1:] {
145+
fromj, _ := txj.From()
146+
147+
if fromi == fromj && txi.Nonce() > txj.Nonce() {
148+
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())
149+
}
150+
}
151+
// Find the previous and next nonce of this account
152+
prev, next := i-1, i+1
153+
for j := i - 1; j >= 0; j-- {
154+
if fromj, _ := txs[j].From(); fromi == fromj {
155+
prev = j
156+
break
157+
}
158+
}
159+
for j := i + 1; j < len(txs); j++ {
160+
if fromj, _ := txs[j].From(); fromi == fromj {
161+
next = j
162+
break
163+
}
164+
}
165+
// Make sure that in between the neighbor nonces, the transaction is correctly positioned price wise
166+
for j := prev + 1; j < next; j++ {
167+
fromj, _ := txs[j].From()
168+
if j < i && txs[j].GasPrice().Cmp(txi.GasPrice()) < 0 {
169+
t.Errorf("invalid gasprice ordering: tx #%d (A=%x P=%v) < tx #%d (A=%x P=%v)", j, fromj[:4], txs[j].GasPrice(), i, fromi[:4], txi.GasPrice())
170+
}
171+
if j > i && txs[j].GasPrice().Cmp(txi.GasPrice()) > 0 {
172+
t.Errorf("invalid gasprice ordering: tx #%d (A=%x P=%v) > tx #%d (A=%x P=%v)", j, fromj[:4], txs[j].GasPrice(), i, fromi[:4], txi.GasPrice())
173+
}
174+
}
175+
}
176+
}

miner/worker.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package miner
1919
import (
2020
"fmt"
2121
"math/big"
22-
"sort"
2322
"sync"
2423
"sync/atomic"
2524
"time"
@@ -495,12 +494,12 @@ func (self *worker) commitNewWork() {
495494

496495
/* //approach 1
497496
transactions := self.eth.TxPool().GetTransactions()
498-
sort.Sort(types.TxByNonce{transactions})
497+
sort.Sort(types.TxByNonce(transactions))
499498
*/
500499

501500
//approach 2
502501
transactions := self.eth.TxPool().GetTransactions()
503-
sort.Sort(types.TxByPriceAndNonce{transactions})
502+
types.SortByPriceAndNonce(transactions)
504503

505504
/* // approach 3
506505
// commit transactions for this run.
@@ -524,8 +523,8 @@ func (self *worker) commitNewWork() {
524523
multiTxOwner = append(multiTxOwner, txs...)
525524
}
526525
}
527-
sort.Sort(types.TxByPrice{singleTxOwner})
528-
sort.Sort(types.TxByNonce{multiTxOwner})
526+
sort.Sort(types.TxByPrice(singleTxOwner))
527+
sort.Sort(types.TxByNonce(multiTxOwner))
529528
transactions := append(singleTxOwner, multiTxOwner...)
530529
*/
531530

0 commit comments

Comments
 (0)