mirror of https://github.com/ethereum/go-ethereum
core/txpool/blobpool: 4844 blob transaction pool (#26940)
* core/blobpool: implement txpool for blob txs * core/txpool: track address reservations to notice any weird bugs * core/txpool/blobpool: add support for in-memory operation for tests * core/txpool/blobpool: fix heap updating after SetGasTip if account is evicted * core/txpool/blobpool: fix eviction order if cheap leading txs are included * core/txpool/blobpool: add note as to why the eviction fields are not inited in reinject * go.mod: pull in inmem billy form upstream * core/txpool/blobpool: fix review commens * core/txpool/blobpool: make heap and heap test deterministic * core/txpool/blobpool: luv u linter * core/txpool: limit blob transactions to 16 per account * core/txpool/blobpool: fix rebase errors * core/txpool/blobpool: luv you linter * go.mod: revert some strange crypto package dep updatespull/27790/head
parent
37b952a4a2
commit
1662228ac6
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,50 @@ |
||||
// Copyright 2022 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package blobpool |
||||
|
||||
import ( |
||||
"github.com/ethereum/go-ethereum/log" |
||||
) |
||||
|
||||
// Config are the configuration parameters of the blob transaction pool.
|
||||
type Config struct { |
||||
Datadir string // Data directory containing the currently executable blobs
|
||||
Datacap uint64 // Soft-cap of database storage (hard cap is larger due to overhead)
|
||||
PriceBump uint64 // Minimum price bump percentage to replace an already existing nonce
|
||||
} |
||||
|
||||
// DefaultConfig contains the default configurations for the transaction pool.
|
||||
var DefaultConfig = Config{ |
||||
Datadir: "blobpool", |
||||
Datacap: 10 * 1024 * 1024 * 1024, |
||||
PriceBump: 100, // either have patience or be aggressive, no mushy ground
|
||||
} |
||||
|
||||
// sanitize checks the provided user configurations and changes anything that's
|
||||
// unreasonable or unworkable.
|
||||
func (config *Config) sanitize() Config { |
||||
conf := *config |
||||
if conf.Datacap < 1 { |
||||
log.Warn("Sanitizing invalid blobpool storage cap", "provided", conf.Datacap, "updated", DefaultConfig.Datacap) |
||||
conf.Datacap = DefaultConfig.Datacap |
||||
} |
||||
if conf.PriceBump < 1 { |
||||
log.Warn("Sanitizing invalid blobpool price bump", "provided", conf.PriceBump, "updated", DefaultConfig.PriceBump) |
||||
conf.PriceBump = DefaultConfig.PriceBump |
||||
} |
||||
return conf |
||||
} |
@ -0,0 +1,146 @@ |
||||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package blobpool |
||||
|
||||
import ( |
||||
"bytes" |
||||
"container/heap" |
||||
"math" |
||||
"sort" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/holiman/uint256" |
||||
) |
||||
|
||||
// evictHeap is a helper data structure to keep track of the cheapest bottleneck
|
||||
// transaction from each account to determine which account to evict from.
|
||||
//
|
||||
// The heap internally tracks a slice of cheapest transactions from each account
|
||||
// and a mapping from addresses to indices for direct removals/udates.
|
||||
//
|
||||
// The goal of the heap is to decide which account has the worst bottleneck to
|
||||
// evict transactions from.
|
||||
type evictHeap struct { |
||||
metas *map[common.Address][]*blobTxMeta // Pointer to the blob pool's index for price retrievals
|
||||
|
||||
basefeeJumps float64 // Pre-calculated absolute dynamic fee jumps for the base fee
|
||||
blobfeeJumps float64 // Pre-calculated absolute dynamic fee jumps for the blob fee
|
||||
|
||||
addrs []common.Address // Heap of addresses to retrieve the cheapest out of
|
||||
index map[common.Address]int // Indices into the heap for replacements
|
||||
} |
||||
|
||||
// newPriceHeap creates a new heap of cheapets accounts in the blob pool to evict
|
||||
// from in case of over saturation.
|
||||
func newPriceHeap(basefee *uint256.Int, blobfee *uint256.Int, index *map[common.Address][]*blobTxMeta) *evictHeap { |
||||
heap := &evictHeap{ |
||||
metas: index, |
||||
index: make(map[common.Address]int), |
||||
} |
||||
// Populate the heap in account sort order. Not really needed in practice,
|
||||
// but it makes the heap initialization deterministic and less annoying to
|
||||
// test in unit tests.
|
||||
addrs := make([]common.Address, 0, len(*index)) |
||||
for addr := range *index { |
||||
addrs = append(addrs, addr) |
||||
} |
||||
sort.Slice(addrs, func(i, j int) bool { return bytes.Compare(addrs[i][:], addrs[j][:]) < 0 }) |
||||
|
||||
for _, addr := range addrs { |
||||
heap.index[addr] = len(heap.addrs) |
||||
heap.addrs = append(heap.addrs, addr) |
||||
} |
||||
heap.reinit(basefee, blobfee, true) |
||||
return heap |
||||
} |
||||
|
||||
// reinit updates the pre-calculated dynamic fee jumps in the price heap and runs
|
||||
// the sorting algorithm from scratch on the entire heap.
|
||||
func (h *evictHeap) reinit(basefee *uint256.Int, blobfee *uint256.Int, force bool) { |
||||
// If the update is mostly the same as the old, don't sort pointlessly
|
||||
basefeeJumps := dynamicFeeJumps(basefee) |
||||
blobfeeJumps := dynamicFeeJumps(blobfee) |
||||
|
||||
if !force && math.Abs(h.basefeeJumps-basefeeJumps) < 0.01 && math.Abs(h.blobfeeJumps-blobfeeJumps) < 0.01 { // TODO(karalabe): 0.01 enough, maybe should be smaller? Maybe this optimization is moot?
|
||||
return |
||||
} |
||||
// One or both of the dynamic fees jumped, resort the pool
|
||||
h.basefeeJumps = basefeeJumps |
||||
h.blobfeeJumps = blobfeeJumps |
||||
|
||||
heap.Init(h) |
||||
} |
||||
|
||||
// Len implements sort.Interface as part of heap.Interface, returning the number
|
||||
// of accounts in the pool which can be considered for eviction.
|
||||
func (h *evictHeap) Len() int { |
||||
return len(h.addrs) |
||||
} |
||||
|
||||
// Less implements sort.Interface as part of heap.Interface, returning which of
|
||||
// the two requested accounts has a cheaper bottleneck.
|
||||
func (h *evictHeap) Less(i, j int) bool { |
||||
txsI := (*(h.metas))[h.addrs[i]] |
||||
txsJ := (*(h.metas))[h.addrs[j]] |
||||
|
||||
lastI := txsI[len(txsI)-1] |
||||
lastJ := txsJ[len(txsJ)-1] |
||||
|
||||
prioI := evictionPriority(h.basefeeJumps, lastI.evictionExecFeeJumps, h.blobfeeJumps, lastI.evictionBlobFeeJumps) |
||||
if prioI > 0 { |
||||
prioI = 0 |
||||
} |
||||
prioJ := evictionPriority(h.basefeeJumps, lastJ.evictionExecFeeJumps, h.blobfeeJumps, lastJ.evictionBlobFeeJumps) |
||||
if prioJ > 0 { |
||||
prioJ = 0 |
||||
} |
||||
if prioI == prioJ { |
||||
return lastI.evictionExecTip.Lt(lastJ.evictionExecTip) |
||||
} |
||||
return prioI < prioJ |
||||
} |
||||
|
||||
// Swap implements sort.Interface as part of heap.Interface, maintaining both the
|
||||
// order of the accounts according to the heap, and the account->item slot mapping
|
||||
// for replacements.
|
||||
func (h *evictHeap) Swap(i, j int) { |
||||
h.index[h.addrs[i]], h.index[h.addrs[j]] = h.index[h.addrs[j]], h.index[h.addrs[i]] |
||||
h.addrs[i], h.addrs[j] = h.addrs[j], h.addrs[i] |
||||
} |
||||
|
||||
// Push implements heap.Interface, appending an item to the end of the account
|
||||
// ordering as well as the address to item slot mapping.
|
||||
func (h *evictHeap) Push(x any) { |
||||
h.index[x.(common.Address)] = len(h.addrs) |
||||
h.addrs = append(h.addrs, x.(common.Address)) |
||||
} |
||||
|
||||
// Pop implements heap.Interface, removing and returning the last element of the
|
||||
// heap.
|
||||
//
|
||||
// Note, use `heap.Pop`, not `evictHeap.Pop`. This method is used by Go's heap,
|
||||
// to provide the functionality, it does not embed it.
|
||||
func (h *evictHeap) Pop() any { |
||||
// Remove the last element from the heap
|
||||
size := len(h.addrs) |
||||
addr := h.addrs[size-1] |
||||
h.addrs = h.addrs[:size-1] |
||||
|
||||
// Unindex the removed element and return
|
||||
delete(h.index, addr) |
||||
return addr |
||||
} |
@ -0,0 +1,320 @@ |
||||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package blobpool |
||||
|
||||
import ( |
||||
"container/heap" |
||||
mrand "math/rand" |
||||
"testing" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/params" |
||||
"github.com/holiman/uint256" |
||||
) |
||||
|
||||
var rand = mrand.New(mrand.NewSource(1)) |
||||
|
||||
// verifyHeapInternals verifies that all accounts present in the index are also
|
||||
// present in the heap and internals are consistent across various indices.
|
||||
func verifyHeapInternals(t *testing.T, evict *evictHeap) { |
||||
t.Helper() |
||||
|
||||
// Ensure that all accounts are present in the heap and no extras
|
||||
seen := make(map[common.Address]struct{}) |
||||
for i, addr := range evict.addrs { |
||||
seen[addr] = struct{}{} |
||||
if _, ok := (*evict.metas)[addr]; !ok { |
||||
t.Errorf("heap contains unexpected address at slot %d: %v", i, addr) |
||||
} |
||||
} |
||||
for addr := range *evict.metas { |
||||
if _, ok := seen[addr]; !ok { |
||||
t.Errorf("heap is missing required address %v", addr) |
||||
} |
||||
} |
||||
if len(evict.addrs) != len(*evict.metas) { |
||||
t.Errorf("heap size %d mismatches metadata size %d", len(evict.addrs), len(*evict.metas)) |
||||
} |
||||
// Ensure that all accounts are present in the heap order index and no extras
|
||||
have := make([]common.Address, len(evict.index)) |
||||
for addr, i := range evict.index { |
||||
have[i] = addr |
||||
} |
||||
if len(have) != len(evict.addrs) { |
||||
t.Errorf("heap index size %d mismatches heap size %d", len(have), len(evict.addrs)) |
||||
} |
||||
for i := 0; i < len(have) && i < len(evict.addrs); i++ { |
||||
if have[i] != evict.addrs[i] { |
||||
t.Errorf("heap index for slot %d mismatches: have %v, want %v", i, have[i], evict.addrs[i]) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Tests that the price heap can correctly sort its set of transactions based on
|
||||
// an input base- and blob fee.
|
||||
func TestPriceHeapSorting(t *testing.T) { |
||||
tests := []struct { |
||||
execTips []uint64 |
||||
execFees []uint64 |
||||
blobFees []uint64 |
||||
|
||||
basefee uint64 |
||||
blobfee uint64 |
||||
|
||||
order []int |
||||
}{ |
||||
// If everything is above the basefee and blobfee, order by miner tip
|
||||
{ |
||||
execTips: []uint64{1, 0, 2}, |
||||
execFees: []uint64{1, 2, 3}, |
||||
blobFees: []uint64{3, 2, 1}, |
||||
basefee: 0, |
||||
blobfee: 0, |
||||
order: []int{1, 0, 2}, |
||||
}, |
||||
// If only basefees are used (blob fee matches with network), return the
|
||||
// ones the furthest below the current basefee, splitting same ones with
|
||||
// the tip. Anything above the basefee should be split by tip.
|
||||
{ |
||||
execTips: []uint64{100, 50, 100, 50, 1, 2, 3}, |
||||
execFees: []uint64{1000, 1000, 500, 500, 2000, 2000, 2000}, |
||||
blobFees: []uint64{0, 0, 0, 0, 0, 0, 0}, |
||||
basefee: 1999, |
||||
blobfee: 0, |
||||
order: []int{3, 2, 1, 0, 4, 5, 6}, |
||||
}, |
||||
// If only blobfees are used (base fee matches with network), return the
|
||||
// ones the furthest below the current blobfee, splitting same ones with
|
||||
// the tip. Anything above the blobfee should be split by tip.
|
||||
{ |
||||
execTips: []uint64{100, 50, 100, 50, 1, 2, 3}, |
||||
execFees: []uint64{0, 0, 0, 0, 0, 0, 0}, |
||||
blobFees: []uint64{1000, 1000, 500, 500, 2000, 2000, 2000}, |
||||
basefee: 0, |
||||
blobfee: 1999, |
||||
order: []int{3, 2, 1, 0, 4, 5, 6}, |
||||
}, |
||||
// If both basefee and blobfee is specified, sort by the larger distance
|
||||
// of the two from the current network conditions, splitting same (loglog)
|
||||
// ones via the tip.
|
||||
//
|
||||
// Basefee: 1000
|
||||
// Blobfee: 100
|
||||
//
|
||||
// Tx #0: (800, 80) - 2 jumps below both => priority -1
|
||||
// Tx #1: (630, 63) - 4 jumps below both => priority -2
|
||||
// Tx #2: (800, 63) - 2 jumps below basefee, 4 jumps below blobfee => priority -2 (blob penalty dominates)
|
||||
// Tx #3: (630, 80) - 4 jumps below basefee, 2 jumps below blobfee => priority -2 (base penalty dominates)
|
||||
//
|
||||
// Txs 1, 2, 3 share the same priority, split via tip, prefer 0 as the best
|
||||
{ |
||||
execTips: []uint64{1, 2, 3, 4}, |
||||
execFees: []uint64{800, 630, 800, 630}, |
||||
blobFees: []uint64{80, 63, 63, 80}, |
||||
basefee: 1000, |
||||
blobfee: 100, |
||||
order: []int{1, 2, 3, 0}, |
||||
}, |
||||
} |
||||
for i, tt := range tests { |
||||
// Create an index of the transactions
|
||||
index := make(map[common.Address][]*blobTxMeta) |
||||
for j := byte(0); j < byte(len(tt.execTips)); j++ { |
||||
addr := common.Address{j} |
||||
|
||||
var ( |
||||
execTip = uint256.NewInt(tt.execTips[j]) |
||||
execFee = uint256.NewInt(tt.execFees[j]) |
||||
blobFee = uint256.NewInt(tt.blobFees[j]) |
||||
|
||||
basefeeJumps = dynamicFeeJumps(execFee) |
||||
blobfeeJumps = dynamicFeeJumps(blobFee) |
||||
) |
||||
index[addr] = []*blobTxMeta{{ |
||||
id: uint64(j), |
||||
size: 128 * 1024, |
||||
nonce: 0, |
||||
execTipCap: execTip, |
||||
execFeeCap: execFee, |
||||
blobFeeCap: blobFee, |
||||
basefeeJumps: basefeeJumps, |
||||
blobfeeJumps: blobfeeJumps, |
||||
evictionExecTip: execTip, |
||||
evictionExecFeeJumps: basefeeJumps, |
||||
evictionBlobFeeJumps: blobfeeJumps, |
||||
}} |
||||
} |
||||
// Create a price heap and check the pop order
|
||||
priceheap := newPriceHeap(uint256.NewInt(tt.basefee), uint256.NewInt(tt.blobfee), &index) |
||||
verifyHeapInternals(t, priceheap) |
||||
|
||||
for j := 0; j < len(tt.order); j++ { |
||||
if next := heap.Pop(priceheap); int(next.(common.Address)[0]) != tt.order[j] { |
||||
t.Errorf("test %d, item %d: order mismatch: have %d, want %d", i, j, next.(common.Address)[0], tt.order[j]) |
||||
} else { |
||||
delete(index, next.(common.Address)) // remove to simulate a correct pool for the test
|
||||
} |
||||
verifyHeapInternals(t, priceheap) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Benchmarks reheaping the entire set of accounts in the blob pool.
|
||||
func BenchmarkPriceHeapReinit1MB(b *testing.B) { benchmarkPriceHeapReinit(b, 1024*1024) } |
||||
func BenchmarkPriceHeapReinit10MB(b *testing.B) { benchmarkPriceHeapReinit(b, 10*1024*1024) } |
||||
func BenchmarkPriceHeapReinit100MB(b *testing.B) { benchmarkPriceHeapReinit(b, 100*1024*1024) } |
||||
func BenchmarkPriceHeapReinit1GB(b *testing.B) { benchmarkPriceHeapReinit(b, 1024*1024*1024) } |
||||
func BenchmarkPriceHeapReinit10GB(b *testing.B) { benchmarkPriceHeapReinit(b, 10*1024*1024*1024) } |
||||
func BenchmarkPriceHeapReinit25GB(b *testing.B) { benchmarkPriceHeapReinit(b, 25*1024*1024*1024) } |
||||
func BenchmarkPriceHeapReinit50GB(b *testing.B) { benchmarkPriceHeapReinit(b, 50*1024*1024*1024) } |
||||
func BenchmarkPriceHeapReinit100GB(b *testing.B) { benchmarkPriceHeapReinit(b, 100*1024*1024*1024) } |
||||
|
||||
func benchmarkPriceHeapReinit(b *testing.B, datacap uint64) { |
||||
// Calculate how many unique transactions we can fit into the provided disk
|
||||
// data cap
|
||||
blobs := datacap / (params.BlobTxBytesPerFieldElement * params.BlobTxFieldElementsPerBlob) |
||||
|
||||
// Create a random set of transactions with random fees. Use a separate account
|
||||
// for each transaction to make it worse case.
|
||||
index := make(map[common.Address][]*blobTxMeta) |
||||
for i := 0; i < int(blobs); i++ { |
||||
var addr common.Address |
||||
rand.Read(addr[:]) |
||||
|
||||
var ( |
||||
execTip = uint256.NewInt(rand.Uint64()) |
||||
execFee = uint256.NewInt(rand.Uint64()) |
||||
blobFee = uint256.NewInt(rand.Uint64()) |
||||
|
||||
basefeeJumps = dynamicFeeJumps(execFee) |
||||
blobfeeJumps = dynamicFeeJumps(blobFee) |
||||
) |
||||
index[addr] = []*blobTxMeta{{ |
||||
id: uint64(i), |
||||
size: 128 * 1024, |
||||
nonce: 0, |
||||
execTipCap: execTip, |
||||
execFeeCap: execFee, |
||||
blobFeeCap: blobFee, |
||||
basefeeJumps: basefeeJumps, |
||||
blobfeeJumps: blobfeeJumps, |
||||
evictionExecTip: execTip, |
||||
evictionExecFeeJumps: basefeeJumps, |
||||
evictionBlobFeeJumps: blobfeeJumps, |
||||
}} |
||||
} |
||||
// Create a price heap and reinit it over and over
|
||||
heap := newPriceHeap(uint256.NewInt(rand.Uint64()), uint256.NewInt(rand.Uint64()), &index) |
||||
|
||||
basefees := make([]*uint256.Int, b.N) |
||||
blobfees := make([]*uint256.Int, b.N) |
||||
for i := 0; i < b.N; i++ { |
||||
basefees[i] = uint256.NewInt(rand.Uint64()) |
||||
blobfees[i] = uint256.NewInt(rand.Uint64()) |
||||
} |
||||
b.ResetTimer() |
||||
b.ReportAllocs() |
||||
for i := 0; i < b.N; i++ { |
||||
heap.reinit(basefees[i], blobfees[i], true) |
||||
} |
||||
} |
||||
|
||||
// Benchmarks overflowing the heap over and over (add and then drop).
|
||||
func BenchmarkPriceHeapOverflow1MB(b *testing.B) { benchmarkPriceHeapOverflow(b, 1024*1024) } |
||||
func BenchmarkPriceHeapOverflow10MB(b *testing.B) { benchmarkPriceHeapOverflow(b, 10*1024*1024) } |
||||
func BenchmarkPriceHeapOverflow100MB(b *testing.B) { benchmarkPriceHeapOverflow(b, 100*1024*1024) } |
||||
func BenchmarkPriceHeapOverflow1GB(b *testing.B) { benchmarkPriceHeapOverflow(b, 1024*1024*1024) } |
||||
func BenchmarkPriceHeapOverflow10GB(b *testing.B) { benchmarkPriceHeapOverflow(b, 10*1024*1024*1024) } |
||||
func BenchmarkPriceHeapOverflow25GB(b *testing.B) { benchmarkPriceHeapOverflow(b, 25*1024*1024*1024) } |
||||
func BenchmarkPriceHeapOverflow50GB(b *testing.B) { benchmarkPriceHeapOverflow(b, 50*1024*1024*1024) } |
||||
func BenchmarkPriceHeapOverflow100GB(b *testing.B) { benchmarkPriceHeapOverflow(b, 100*1024*1024*1024) } |
||||
|
||||
func benchmarkPriceHeapOverflow(b *testing.B, datacap uint64) { |
||||
// Calculate how many unique transactions we can fit into the provided disk
|
||||
// data cap
|
||||
blobs := datacap / (params.BlobTxBytesPerFieldElement * params.BlobTxFieldElementsPerBlob) |
||||
|
||||
// Create a random set of transactions with random fees. Use a separate account
|
||||
// for each transaction to make it worse case.
|
||||
index := make(map[common.Address][]*blobTxMeta) |
||||
for i := 0; i < int(blobs); i++ { |
||||
var addr common.Address |
||||
rand.Read(addr[:]) |
||||
|
||||
var ( |
||||
execTip = uint256.NewInt(rand.Uint64()) |
||||
execFee = uint256.NewInt(rand.Uint64()) |
||||
blobFee = uint256.NewInt(rand.Uint64()) |
||||
|
||||
basefeeJumps = dynamicFeeJumps(execFee) |
||||
blobfeeJumps = dynamicFeeJumps(blobFee) |
||||
) |
||||
index[addr] = []*blobTxMeta{{ |
||||
id: uint64(i), |
||||
size: 128 * 1024, |
||||
nonce: 0, |
||||
execTipCap: execTip, |
||||
execFeeCap: execFee, |
||||
blobFeeCap: blobFee, |
||||
basefeeJumps: basefeeJumps, |
||||
blobfeeJumps: blobfeeJumps, |
||||
evictionExecTip: execTip, |
||||
evictionExecFeeJumps: basefeeJumps, |
||||
evictionBlobFeeJumps: blobfeeJumps, |
||||
}} |
||||
} |
||||
// Create a price heap and overflow it over and over
|
||||
evict := newPriceHeap(uint256.NewInt(rand.Uint64()), uint256.NewInt(rand.Uint64()), &index) |
||||
var ( |
||||
addrs = make([]common.Address, b.N) |
||||
metas = make([]*blobTxMeta, b.N) |
||||
) |
||||
for i := 0; i < b.N; i++ { |
||||
rand.Read(addrs[i][:]) |
||||
|
||||
var ( |
||||
execTip = uint256.NewInt(rand.Uint64()) |
||||
execFee = uint256.NewInt(rand.Uint64()) |
||||
blobFee = uint256.NewInt(rand.Uint64()) |
||||
|
||||
basefeeJumps = dynamicFeeJumps(execFee) |
||||
blobfeeJumps = dynamicFeeJumps(blobFee) |
||||
) |
||||
metas[i] = &blobTxMeta{ |
||||
id: uint64(int(blobs) + i), |
||||
size: 128 * 1024, |
||||
nonce: 0, |
||||
execTipCap: execTip, |
||||
execFeeCap: execFee, |
||||
blobFeeCap: blobFee, |
||||
basefeeJumps: basefeeJumps, |
||||
blobfeeJumps: blobfeeJumps, |
||||
evictionExecTip: execTip, |
||||
evictionExecFeeJumps: basefeeJumps, |
||||
evictionBlobFeeJumps: blobfeeJumps, |
||||
} |
||||
} |
||||
b.ResetTimer() |
||||
b.ReportAllocs() |
||||
for i := 0; i < b.N; i++ { |
||||
index[addrs[i]] = []*blobTxMeta{metas[i]} |
||||
heap.Push(evict, addrs[i]) |
||||
|
||||
drop := heap.Pop(evict) |
||||
delete(index, drop.(common.Address)) |
||||
} |
||||
} |
@ -0,0 +1,44 @@ |
||||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package blobpool |
||||
|
||||
import ( |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core/state" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/params" |
||||
) |
||||
|
||||
// BlockChain defines the minimal set of methods needed to back a blob pool with
|
||||
// a chain. Exists to allow mocking the live chain out of tests.
|
||||
type BlockChain interface { |
||||
// Config retrieves the chain's fork configuration.
|
||||
Config() *params.ChainConfig |
||||
|
||||
// CurrentBlock returns the current head of the chain.
|
||||
CurrentBlock() *types.Header |
||||
|
||||
// CurrentFinalBlock returns the current block below which blobs should not
|
||||
// be maintained anymore for reorg purposes.
|
||||
CurrentFinalBlock() *types.Header |
||||
|
||||
// GetBlock retrieves a specific block, used during pool resets.
|
||||
GetBlock(hash common.Hash, number uint64) *types.Block |
||||
|
||||
// StateAt returns a state database for a given root hash (generally the head).
|
||||
StateAt(root common.Hash) (*state.StateDB, error) |
||||
} |
@ -0,0 +1,258 @@ |
||||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package blobpool |
||||
|
||||
import ( |
||||
"errors" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/crypto/kzg4844" |
||||
"github.com/ethereum/go-ethereum/log" |
||||
"github.com/ethereum/go-ethereum/rlp" |
||||
"github.com/holiman/billy" |
||||
) |
||||
|
||||
// limboBlob is a wrapper around an opaque blobset that also contains the tx hash
|
||||
// to which it belongs as well as the block number in which it was included for
|
||||
// finality eviction.
|
||||
type limboBlob struct { |
||||
Owner common.Hash // Owner transaction's hash to support resurrecting reorged txs
|
||||
Block uint64 // Block in which the blob transaction was included
|
||||
|
||||
Blobs []kzg4844.Blob // The opaque blobs originally part of the transaction
|
||||
Commits []kzg4844.Commitment // The commitments for the original blobs
|
||||
Proofs []kzg4844.Proof // The proofs verifying the commitments
|
||||
} |
||||
|
||||
// limbo is a light, indexed database to temporarily store recently included
|
||||
// blobs until they are finalized. The purpose is to support small reorgs, which
|
||||
// would require pulling back up old blobs (which aren't part of the chain).
|
||||
//
|
||||
// TODO(karalabe): Currently updating the inclusion block of a blob needs a full db rewrite. Can we do without?
|
||||
type limbo struct { |
||||
store billy.Database // Persistent data store for limboed blobs
|
||||
|
||||
index map[common.Hash]uint64 // Mappings from tx hashes to datastore ids
|
||||
groups map[uint64]map[uint64]common.Hash // Set of txs included in past blocks
|
||||
} |
||||
|
||||
// newLimbo opens and indexes a set of limboed blob transactions.
|
||||
func newLimbo(datadir string) (*limbo, error) { |
||||
l := &limbo{ |
||||
index: make(map[common.Hash]uint64), |
||||
groups: make(map[uint64]map[uint64]common.Hash), |
||||
} |
||||
// Index all limboed blobs on disk and delete anything inprocessable
|
||||
var fails []uint64 |
||||
index := func(id uint64, size uint32, data []byte) { |
||||
if l.parseBlob(id, data) != nil { |
||||
fails = append(fails, id) |
||||
} |
||||
} |
||||
store, err := billy.Open(billy.Options{Path: datadir}, newSlotter(), index) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
l.store = store |
||||
|
||||
if len(fails) > 0 { |
||||
log.Warn("Dropping invalidated limboed blobs", "ids", fails) |
||||
for _, id := range fails { |
||||
if err := l.store.Delete(id); err != nil { |
||||
l.Close() |
||||
return nil, err |
||||
} |
||||
} |
||||
} |
||||
return l, nil |
||||
} |
||||
|
||||
// Close closes down the underlying persistent store.
|
||||
func (l *limbo) Close() error { |
||||
return l.store.Close() |
||||
} |
||||
|
||||
// parseBlob is a callback method on limbo creation that gets called for each
|
||||
// limboed blob on disk to create the in-memory metadata index.
|
||||
func (l *limbo) parseBlob(id uint64, data []byte) error { |
||||
item := new(limboBlob) |
||||
if err := rlp.DecodeBytes(data, item); err != nil { |
||||
// This path is impossible unless the disk data representation changes
|
||||
// across restarts. For that ever unprobable case, recover gracefully
|
||||
// by ignoring this data entry.
|
||||
log.Error("Failed to decode blob limbo entry", "id", id, "err", err) |
||||
return err |
||||
} |
||||
if _, ok := l.index[item.Owner]; ok { |
||||
// This path is impossible, unless due to a programming error a blob gets
|
||||
// inserted into the limbo which was already part of if. Recover gracefully
|
||||
// by ignoring this data entry.
|
||||
log.Error("Dropping duplicate blob limbo entry", "owner", item.Owner, "id", id) |
||||
return errors.New("duplicate blob") |
||||
} |
||||
l.index[item.Owner] = id |
||||
|
||||
if _, ok := l.groups[item.Block]; !ok { |
||||
l.groups[item.Block] = make(map[uint64]common.Hash) |
||||
} |
||||
l.groups[item.Block][id] = item.Owner |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// finalize evicts all blobs belonging to a recently finalized block or older.
|
||||
func (l *limbo) finalize(final *types.Header) { |
||||
// Just in case there's no final block yet (network not yet merged, weird
|
||||
// restart, sethead, etc), fail gracefully.
|
||||
if final == nil { |
||||
log.Error("Nil finalized block cannot evict old blobs") |
||||
return |
||||
} |
||||
for block, ids := range l.groups { |
||||
if block > final.Number.Uint64() { |
||||
continue |
||||
} |
||||
for id, owner := range ids { |
||||
if err := l.store.Delete(id); err != nil { |
||||
log.Error("Failed to drop finalized blob", "block", block, "id", id, "err", err) |
||||
} |
||||
delete(l.index, owner) |
||||
} |
||||
delete(l.groups, block) |
||||
} |
||||
} |
||||
|
||||
// push stores a new blob transaction into the limbo, waiting until finality for
|
||||
// it to be automatically evicted.
|
||||
func (l *limbo) push(tx common.Hash, block uint64, blobs []kzg4844.Blob, commits []kzg4844.Commitment, proofs []kzg4844.Proof) error { |
||||
// If the blobs are already tracked by the limbo, consider it a programming
|
||||
// error. There's not much to do against it, but be loud.
|
||||
if _, ok := l.index[tx]; ok { |
||||
log.Error("Limbo cannot push already tracked blobs", "tx", tx) |
||||
return errors.New("already tracked blob transaction") |
||||
} |
||||
if err := l.setAndIndex(tx, block, blobs, commits, proofs); err != nil { |
||||
log.Error("Failed to set and index liboed blobs", "tx", tx, "err", err) |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// pull retrieves a previously pushed set of blobs back from the limbo, removing
|
||||
// it at the same time. This method should be used when a previously included blob
|
||||
// transaction gets reorged out.
|
||||
func (l *limbo) pull(tx common.Hash) ([]kzg4844.Blob, []kzg4844.Commitment, []kzg4844.Proof, error) { |
||||
// If the blobs are not tracked by the limbo, there's not much to do. This
|
||||
// can happen for example if a blob transaction is mined without pushing it
|
||||
// into the network first.
|
||||
id, ok := l.index[tx] |
||||
if !ok { |
||||
log.Trace("Limbo cannot pull non-tracked blobs", "tx", tx) |
||||
return nil, nil, nil, errors.New("unseen blob transaction") |
||||
} |
||||
item, err := l.getAndDrop(id) |
||||
if err != nil { |
||||
log.Error("Failed to get and drop limboed blobs", "tx", tx, "id", id, "err", err) |
||||
return nil, nil, nil, err |
||||
} |
||||
return item.Blobs, item.Commits, item.Proofs, nil |
||||
} |
||||
|
||||
// update changes the block number under which a blob transaction is tracked. This
|
||||
// method should be used when a reorg changes a transaction's inclusion block.
|
||||
//
|
||||
// The method may log errors for various unexpcted scenarios but will not return
|
||||
// any of it since there's no clear error case. Some errors may be due to coding
|
||||
// issues, others caused by signers mining MEV stuff or swapping transactions. In
|
||||
// all cases, the pool needs to continue operating.
|
||||
func (l *limbo) update(tx common.Hash, block uint64) { |
||||
// If the blobs are not tracked by the limbo, there's not much to do. This
|
||||
// can happen for example if a blob transaction is mined without pushing it
|
||||
// into the network first.
|
||||
id, ok := l.index[tx] |
||||
if !ok { |
||||
log.Trace("Limbo cannot update non-tracked blobs", "tx", tx) |
||||
return |
||||
} |
||||
// If there was no change in the blob's inclusion block, don't mess around
|
||||
// with heavy database operations.
|
||||
if _, ok := l.groups[block][id]; ok { |
||||
log.Trace("Blob transaction unchanged in limbo", "tx", tx, "block", block) |
||||
return |
||||
} |
||||
// Retrieve the old blobs from the data store and write tehm back with a new
|
||||
// block number. IF anything fails, there's not much to do, go on.
|
||||
item, err := l.getAndDrop(id) |
||||
if err != nil { |
||||
log.Error("Failed to get and drop limboed blobs", "tx", tx, "id", id, "err", err) |
||||
return |
||||
} |
||||
if err := l.setAndIndex(tx, block, item.Blobs, item.Commits, item.Proofs); err != nil { |
||||
log.Error("Failed to set and index limboed blobs", "tx", tx, "err", err) |
||||
return |
||||
} |
||||
log.Trace("Blob transaction updated in limbo", "tx", tx, "old-block", item.Block, "new-block", block) |
||||
} |
||||
|
||||
// getAndDrop retrieves a blob item from the limbo store and deletes it both from
|
||||
// the store and indices.
|
||||
func (l *limbo) getAndDrop(id uint64) (*limboBlob, error) { |
||||
data, err := l.store.Get(id) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
item := new(limboBlob) |
||||
if err = rlp.DecodeBytes(data, item); err != nil { |
||||
return nil, err |
||||
} |
||||
delete(l.index, item.Owner) |
||||
delete(l.groups[item.Block], id) |
||||
if len(l.groups[item.Block]) == 0 { |
||||
delete(l.groups, item.Block) |
||||
} |
||||
if err := l.store.Delete(id); err != nil { |
||||
return nil, err |
||||
} |
||||
return item, nil |
||||
} |
||||
|
||||
// setAndIndex assembles a limbo blob database entry and stores it, also updating
|
||||
// the in-memory indices.
|
||||
func (l *limbo) setAndIndex(tx common.Hash, block uint64, blobs []kzg4844.Blob, commits []kzg4844.Commitment, proofs []kzg4844.Proof) error { |
||||
item := &limboBlob{ |
||||
Owner: tx, |
||||
Block: block, |
||||
Blobs: blobs, |
||||
Commits: commits, |
||||
Proofs: proofs, |
||||
} |
||||
data, err := rlp.EncodeToBytes(item) |
||||
if err != nil { |
||||
panic(err) // cannot happen runtime, dev error
|
||||
} |
||||
id, err := l.store.Put(data) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
l.index[tx] = id |
||||
if _, ok := l.groups[block]; !ok { |
||||
l.groups[block] = make(map[uint64]common.Hash) |
||||
} |
||||
l.groups[block][id] = tx |
||||
return nil |
||||
} |
@ -0,0 +1,78 @@ |
||||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package blobpool |
||||
|
||||
import "github.com/ethereum/go-ethereum/metrics" |
||||
|
||||
var ( |
||||
// datacapGauge tracks the user's configured capacity for the blob pool. It
|
||||
// is mostly a way to expose/debug issues.
|
||||
datacapGauge = metrics.NewRegisteredGauge("blobpool/datacap", nil) |
||||
|
||||
// The below metrics track the per-datastore metrics for the primary blob
|
||||
// store and the temporary limbo store.
|
||||
datausedGauge = metrics.NewRegisteredGauge("blobpool/dataused", nil) |
||||
datarealGauge = metrics.NewRegisteredGauge("blobpool/datareal", nil) |
||||
slotusedGauge = metrics.NewRegisteredGauge("blobpool/slotused", nil) |
||||
|
||||
limboDatausedGauge = metrics.NewRegisteredGauge("blobpool/limbo/dataused", nil) |
||||
limboDatarealGauge = metrics.NewRegisteredGauge("blobpool/limbo/datareal", nil) |
||||
limboSlotusedGauge = metrics.NewRegisteredGauge("blobpool/limbo/slotused", nil) |
||||
|
||||
// The below metrics track the per-shelf metrics for the primary blob store
|
||||
// and the temporary limbo store.
|
||||
shelfDatausedGaugeName = "blobpool/shelf-%d/dataused" |
||||
shelfDatagapsGaugeName = "blobpool/shelf-%d/datagaps" |
||||
shelfSlotusedGaugeName = "blobpool/shelf-%d/slotused" |
||||
shelfSlotgapsGaugeName = "blobpool/shelf-%d/slotgaps" |
||||
|
||||
limboShelfDatausedGaugeName = "blobpool/limbo/shelf-%d/dataused" |
||||
limboShelfDatagapsGaugeName = "blobpool/limbo/shelf-%d/datagaps" |
||||
limboShelfSlotusedGaugeName = "blobpool/limbo/shelf-%d/slotused" |
||||
limboShelfSlotgapsGaugeName = "blobpool/limbo/shelf-%d/slotgaps" |
||||
|
||||
// The oversized metrics aggregate the shelf stats above the max blob count
|
||||
// limits to track transactions that are just huge, but don't contain blobs.
|
||||
//
|
||||
// There are no oversized data in the limbo, it only contains blobs and some
|
||||
// constant metadata.
|
||||
oversizedDatausedGauge = metrics.NewRegisteredGauge("blobpool/oversized/dataused", nil) |
||||
oversizedDatagapsGauge = metrics.NewRegisteredGauge("blobpool/oversized/datagaps", nil) |
||||
oversizedSlotusedGauge = metrics.NewRegisteredGauge("blobpool/oversized/slotused", nil) |
||||
oversizedSlotgapsGauge = metrics.NewRegisteredGauge("blobpool/oversized/slotgaps", nil) |
||||
|
||||
// basefeeGauge and blobfeeGauge track the current network 1559 base fee and
|
||||
// 4844 blob fee respectively.
|
||||
basefeeGauge = metrics.NewRegisteredGauge("blobpool/basefee", nil) |
||||
blobfeeGauge = metrics.NewRegisteredGauge("blobpool/blobfee", nil) |
||||
|
||||
// pooltipGauge is the configurable miner tip to permit a transaction into
|
||||
// the pool.
|
||||
pooltipGauge = metrics.NewRegisteredGauge("blobpool/pooltip", nil) |
||||
|
||||
// addwait/time, resetwait/time and getwait/time track the rough health of
|
||||
// the pool and wether or not it's capable of keeping up with the load from
|
||||
// the network.
|
||||
addwaitHist = metrics.NewRegisteredHistogram("blobpool/addwait", nil, metrics.NewExpDecaySample(1028, 0.015)) |
||||
addtimeHist = metrics.NewRegisteredHistogram("blobpool/addtime", nil, metrics.NewExpDecaySample(1028, 0.015)) |
||||
getwaitHist = metrics.NewRegisteredHistogram("blobpool/getwait", nil, metrics.NewExpDecaySample(1028, 0.015)) |
||||
gettimeHist = metrics.NewRegisteredHistogram("blobpool/gettime", nil, metrics.NewExpDecaySample(1028, 0.015)) |
||||
pendwaitHist = metrics.NewRegisteredHistogram("blobpool/pendwait", nil, metrics.NewExpDecaySample(1028, 0.015)) |
||||
pendtimeHist = metrics.NewRegisteredHistogram("blobpool/pendtime", nil, metrics.NewExpDecaySample(1028, 0.015)) |
||||
resetwaitHist = metrics.NewRegisteredHistogram("blobpool/resetwait", nil, metrics.NewExpDecaySample(1028, 0.015)) |
||||
resettimeHist = metrics.NewRegisteredHistogram("blobpool/resettime", nil, metrics.NewExpDecaySample(1028, 0.015)) |
||||
) |
@ -0,0 +1,90 @@ |
||||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package blobpool |
||||
|
||||
import ( |
||||
"math" |
||||
"math/bits" |
||||
|
||||
"github.com/holiman/uint256" |
||||
) |
||||
|
||||
// log2_1_125 is used in the eviction priority calculation.
|
||||
var log2_1_125 = math.Log2(1.125) |
||||
|
||||
// evictionPriority calculates the eviction priority based on the algorithm
|
||||
// described in the BlobPool docs for a both fee components.
|
||||
//
|
||||
// This method takes about 8ns on a very recent laptop CPU, recalculating about
|
||||
// 125 million transaction priority values per second.
|
||||
func evictionPriority(basefeeJumps float64, txBasefeeJumps, blobfeeJumps, txBlobfeeJumps float64) int { |
||||
var ( |
||||
basefeePriority = evictionPriority1D(basefeeJumps, txBasefeeJumps) |
||||
blobfeePriority = evictionPriority1D(blobfeeJumps, txBlobfeeJumps) |
||||
) |
||||
if basefeePriority < blobfeePriority { |
||||
return basefeePriority |
||||
} |
||||
return blobfeePriority |
||||
} |
||||
|
||||
// evictionPriority1D calculates the eviction priority based on the algorithm
|
||||
// described in the BlobPool docs for a single fee component.
|
||||
func evictionPriority1D(basefeeJumps float64, txfeeJumps float64) int { |
||||
jumps := txfeeJumps - basefeeJumps |
||||
if int(jumps) == 0 { |
||||
return 0 // can't log2 0
|
||||
} |
||||
if jumps < 0 { |
||||
return -intLog2(uint(-math.Floor(jumps))) |
||||
} |
||||
return intLog2(uint(math.Ceil(jumps))) |
||||
} |
||||
|
||||
// dynamicFeeJumps calculates the log1.125(fee), namely the number of fee jumps
|
||||
// needed to reach the requested one. We only use it when calculating the jumps
|
||||
// between 2 fees, so it doesn't matter from what exact number with returns.
|
||||
// it returns the result from (0, 1, 1.125).
|
||||
//
|
||||
// This method is very expensive, taking about 75ns on a very recent laptop CPU,
|
||||
// but the result does not change with the lifetime of a transaction, so it can
|
||||
// be cached.
|
||||
func dynamicFeeJumps(fee *uint256.Int) float64 { |
||||
if fee.IsZero() { |
||||
return 0 // can't log2 zero, should never happen outside tests, but don't choke
|
||||
} |
||||
return math.Log2(fee.Float64()) / log2_1_125 |
||||
} |
||||
|
||||
// intLog2 is a helper to calculate the integral part of a log2 of an unsigned
|
||||
// integer. It is a very specific calculation that's not particularly useful in
|
||||
// general, but it's what we need here (it's fast).
|
||||
func intLog2(n uint) int { |
||||
switch { |
||||
case n == 0: |
||||
panic("log2(0) is undefined") |
||||
|
||||
case n < 2048: |
||||
return bits.UintSize - bits.LeadingZeros(n) - 1 |
||||
|
||||
default: |
||||
// The input is log1.125(uint256) = log2(uint256) / log2(1.125). At the
|
||||
// most extreme, log2(uint256) will be a bit below 257, and the constant
|
||||
// log2(1.125) ~= 0.17. The larges input thus is ~257 / ~0.17 ~= ~1511.
|
||||
panic("dynamic fee jump diffs cannot reach this") |
||||
} |
||||
} |
@ -0,0 +1,87 @@ |
||||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package blobpool |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/holiman/uint256" |
||||
) |
||||
|
||||
// Tests that the priority fees are calculated correctly as the log2 of the fee
|
||||
// jumps needed to go from the base fee to the tx's fee cap.
|
||||
func TestPriorityCalculation(t *testing.T) { |
||||
tests := []struct { |
||||
basefee uint64 |
||||
txfee uint64 |
||||
result int |
||||
}{ |
||||
{basefee: 7, txfee: 10, result: 2}, // 3.02 jumps, 4 ceil, 2 log2
|
||||
{basefee: 17_200_000_000, txfee: 17_200_000_000, result: 0}, // 0 jumps, special case 0 log2
|
||||
{basefee: 9_853_941_692, txfee: 11_085_092_510, result: 0}, // 0.99 jumps, 1 ceil, 0 log2
|
||||
{basefee: 11_544_106_391, txfee: 10_356_781_100, result: 0}, // -0.92 jumps, -1 floor, 0 log2
|
||||
{basefee: 17_200_000_000, txfee: 7, result: -7}, // -183.57 jumps, -184 floor, -7 log2
|
||||
{basefee: 7, txfee: 17_200_000_000, result: 7}, // 183.57 jumps, 184 ceil, 7 log2
|
||||
} |
||||
for i, tt := range tests { |
||||
var ( |
||||
baseJumps = dynamicFeeJumps(uint256.NewInt(tt.basefee)) |
||||
feeJumps = dynamicFeeJumps(uint256.NewInt(tt.txfee)) |
||||
) |
||||
if prio := evictionPriority1D(baseJumps, feeJumps); prio != tt.result { |
||||
t.Errorf("test %d priority mismatch: have %d, want %d", i, prio, tt.result) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Benchmarks how many dynamic fee jump values can be done.
|
||||
func BenchmarkDynamicFeeJumpCalculation(b *testing.B) { |
||||
fees := make([]*uint256.Int, b.N) |
||||
for i := 0; i < b.N; i++ { |
||||
fees[i] = uint256.NewInt(rand.Uint64()) |
||||
} |
||||
b.ResetTimer() |
||||
b.ReportAllocs() |
||||
for i := 0; i < b.N; i++ { |
||||
dynamicFeeJumps(fees[i]) |
||||
} |
||||
} |
||||
|
||||
// Benchmarks how many priority recalculations can be done.
|
||||
func BenchmarkPriorityCalculation(b *testing.B) { |
||||
// The basefee and blob fee is constant for all transactions across a block,
|
||||
// so we can assume theit absolute jump counts can be pre-computed.
|
||||
basefee := uint256.NewInt(17_200_000_000) // 17.2 Gwei is the 22.03.2023 zero-emission basefee, random number
|
||||
blobfee := uint256.NewInt(123_456_789_000) // Completely random, no idea what this will be
|
||||
|
||||
basefeeJumps := dynamicFeeJumps(basefee) |
||||
blobfeeJumps := dynamicFeeJumps(blobfee) |
||||
|
||||
// The transaction's fee cap and blob fee cap are constant across the life
|
||||
// of the transaction, so we can pre-calculate and cache them.
|
||||
txBasefeeJumps := make([]float64, b.N) |
||||
txBlobfeeJumps := make([]float64, b.N) |
||||
for i := 0; i < b.N; i++ { |
||||
txBasefeeJumps[i] = dynamicFeeJumps(uint256.NewInt(rand.Uint64())) |
||||
txBlobfeeJumps[i] = dynamicFeeJumps(uint256.NewInt(rand.Uint64())) |
||||
} |
||||
b.ResetTimer() |
||||
b.ReportAllocs() |
||||
for i := 0; i < b.N; i++ { |
||||
evictionPriority(basefeeJumps, txBasefeeJumps[i], blobfeeJumps, txBlobfeeJumps[i]) |
||||
} |
||||
} |
@ -0,0 +1,38 @@ |
||||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package blobpool |
||||
|
||||
// newSlotter creates a helper method for the Billy datastore that returns the
|
||||
// individual shelf sizes used to store transactions in.
|
||||
//
|
||||
// The slotter will create shelves for each possible blob count + some tx metadata
|
||||
// wiggle room, up to the max permitted limits.
|
||||
//
|
||||
// The slotter also creates a shelf for 0-blob transactions. Whilst those are not
|
||||
// allowed in the current protocol, having an empty shelf is not a relevant use
|
||||
// of resources, but it makes stress testing with junk transactions simpler.
|
||||
func newSlotter() func() (uint32, bool) { |
||||
slotsize := uint32(txAvgSize) |
||||
slotsize -= uint32(blobSize) // underflows, it's ok, will overflow back in the first return
|
||||
|
||||
return func() (size uint32, done bool) { |
||||
slotsize += blobSize |
||||
finished := slotsize > maxBlobsPerTransaction*blobSize+txMaxSize |
||||
|
||||
return slotsize, finished |
||||
} |
||||
} |
@ -0,0 +1,58 @@ |
||||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package blobpool |
||||
|
||||
import "testing" |
||||
|
||||
// Tests that the slotter creates the expected database shelves.
|
||||
func TestNewSlotter(t *testing.T) { |
||||
// Generate the database shelve sizes
|
||||
slotter := newSlotter() |
||||
|
||||
var shelves []uint32 |
||||
for { |
||||
shelf, done := slotter() |
||||
shelves = append(shelves, shelf) |
||||
if done { |
||||
break |
||||
} |
||||
} |
||||
// Compare the database shelves to the expected ones
|
||||
want := []uint32{ |
||||
0*blobSize + txAvgSize, // 0 blob + some expected tx infos
|
||||
1*blobSize + txAvgSize, // 1 blob + some expected tx infos
|
||||
2*blobSize + txAvgSize, // 2 blob + some expected tx infos (could be fewer blobs and more tx data)
|
||||
3*blobSize + txAvgSize, // 3 blob + some expected tx infos (could be fewer blobs and more tx data)
|
||||
4*blobSize + txAvgSize, // 4 blob + some expected tx infos (could be fewer blobs and more tx data)
|
||||
5*blobSize + txAvgSize, // 1-4 blobs + unexpectedly large tx infos < 4 blobs + max tx metadata size
|
||||
6*blobSize + txAvgSize, // 1-4 blobs + unexpectedly large tx infos < 4 blobs + max tx metadata size
|
||||
7*blobSize + txAvgSize, // 1-4 blobs + unexpectedly large tx infos < 4 blobs + max tx metadata size
|
||||
8*blobSize + txAvgSize, // 1-4 blobs + unexpectedly large tx infos < 4 blobs + max tx metadata size
|
||||
9*blobSize + txAvgSize, // 1-4 blobs + unexpectedly large tx infos < 4 blobs + max tx metadata size
|
||||
10*blobSize + txAvgSize, // 1-4 blobs + unexpectedly large tx infos < 4 blobs + max tx metadata size
|
||||
11*blobSize + txAvgSize, // 1-4 blobs + unexpectedly large tx infos < 4 blobs + max tx metadata size
|
||||
12*blobSize + txAvgSize, // 1-4 blobs + unexpectedly large tx infos >= 4 blobs + max tx metadata size
|
||||
} |
||||
if len(shelves) != len(want) { |
||||
t.Errorf("shelves count mismatch: have %d, want %d", len(shelves), len(want)) |
||||
} |
||||
for i := 0; i < len(shelves) && i < len(want); i++ { |
||||
if shelves[i] != want[i] { |
||||
t.Errorf("shelf %d mismatch: have %d, want %d", i, shelves[i], want[i]) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,147 @@ |
||||
// Copyright 2014 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package miner |
||||
|
||||
import ( |
||||
"container/heap" |
||||
"math/big" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/common/math" |
||||
"github.com/ethereum/go-ethereum/core/txpool" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
) |
||||
|
||||
// txWithMinerFee wraps a transaction with its gas price or effective miner gasTipCap
|
||||
type txWithMinerFee struct { |
||||
tx *txpool.LazyTransaction |
||||
from common.Address |
||||
fees *big.Int |
||||
} |
||||
|
||||
// newTxWithMinerFee creates a wrapped transaction, calculating the effective
|
||||
// miner gasTipCap if a base fee is provided.
|
||||
// Returns error in case of a negative effective miner gasTipCap.
|
||||
func newTxWithMinerFee(tx *txpool.LazyTransaction, from common.Address, baseFee *big.Int) (*txWithMinerFee, error) { |
||||
tip := new(big.Int).Set(tx.GasTipCap) |
||||
if baseFee != nil { |
||||
if tx.GasFeeCap.Cmp(baseFee) < 0 { |
||||
return nil, types.ErrGasFeeCapTooLow |
||||
} |
||||
tip = math.BigMin(tx.GasTipCap, new(big.Int).Sub(tx.GasFeeCap, baseFee)) |
||||
} |
||||
return &txWithMinerFee{ |
||||
tx: tx, |
||||
from: from, |
||||
fees: tip, |
||||
}, nil |
||||
} |
||||
|
||||
// txByPriceAndTime implements both the sort and the heap interface, making it useful
|
||||
// for all at once sorting as well as individually adding and removing elements.
|
||||
type txByPriceAndTime []*txWithMinerFee |
||||
|
||||
func (s txByPriceAndTime) Len() int { return len(s) } |
||||
func (s txByPriceAndTime) Less(i, j int) bool { |
||||
// If the prices are equal, use the time the transaction was first seen for
|
||||
// deterministic sorting
|
||||
cmp := s[i].fees.Cmp(s[j].fees) |
||||
if cmp == 0 { |
||||
return s[i].tx.Time.Before(s[j].tx.Time) |
||||
} |
||||
return cmp > 0 |
||||
} |
||||
func (s txByPriceAndTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } |
||||
|
||||
func (s *txByPriceAndTime) Push(x interface{}) { |
||||
*s = append(*s, x.(*txWithMinerFee)) |
||||
} |
||||
|
||||
func (s *txByPriceAndTime) Pop() interface{} { |
||||
old := *s |
||||
n := len(old) |
||||
x := old[n-1] |
||||
old[n-1] = nil |
||||
*s = old[0 : n-1] |
||||
return x |
||||
} |
||||
|
||||
// transactionsByPriceAndNonce represents a set of transactions that can return
|
||||
// transactions in a profit-maximizing sorted order, while supporting removing
|
||||
// entire batches of transactions for non-executable accounts.
|
||||
type transactionsByPriceAndNonce struct { |
||||
txs map[common.Address][]*txpool.LazyTransaction // Per account nonce-sorted list of transactions
|
||||
heads txByPriceAndTime // Next transaction for each unique account (price heap)
|
||||
signer types.Signer // Signer for the set of transactions
|
||||
baseFee *big.Int // Current base fee
|
||||
} |
||||
|
||||
// newTransactionsByPriceAndNonce creates a transaction set that can retrieve
|
||||
// price sorted transactions in a nonce-honouring way.
|
||||
//
|
||||
// Note, the input map is reowned so the caller should not interact any more with
|
||||
// if after providing it to the constructor.
|
||||
func newTransactionsByPriceAndNonce(signer types.Signer, txs map[common.Address][]*txpool.LazyTransaction, baseFee *big.Int) *transactionsByPriceAndNonce { |
||||
// Initialize a price and received time based heap with the head transactions
|
||||
heads := make(txByPriceAndTime, 0, len(txs)) |
||||
for from, accTxs := range txs { |
||||
wrapped, err := newTxWithMinerFee(accTxs[0], from, baseFee) |
||||
if err != nil { |
||||
delete(txs, from) |
||||
continue |
||||
} |
||||
heads = append(heads, wrapped) |
||||
txs[from] = accTxs[1:] |
||||
} |
||||
heap.Init(&heads) |
||||
|
||||
// Assemble and return the transaction set
|
||||
return &transactionsByPriceAndNonce{ |
||||
txs: txs, |
||||
heads: heads, |
||||
signer: signer, |
||||
baseFee: baseFee, |
||||
} |
||||
} |
||||
|
||||
// Peek returns the next transaction by price.
|
||||
func (t *transactionsByPriceAndNonce) Peek() *txpool.LazyTransaction { |
||||
if len(t.heads) == 0 { |
||||
return nil |
||||
} |
||||
return t.heads[0].tx |
||||
} |
||||
|
||||
// Shift replaces the current best head with the next one from the same account.
|
||||
func (t *transactionsByPriceAndNonce) Shift() { |
||||
acc := t.heads[0].from |
||||
if txs, ok := t.txs[acc]; ok && len(txs) > 0 { |
||||
if wrapped, err := newTxWithMinerFee(txs[0], acc, t.baseFee); err == nil { |
||||
t.heads[0], t.txs[acc] = wrapped, txs[1:] |
||||
heap.Fix(&t.heads, 0) |
||||
return |
||||
} |
||||
} |
||||
heap.Pop(&t.heads) |
||||
} |
||||
|
||||
// Pop removes the best transaction, *not* replacing it with the next one from
|
||||
// the same account. This should be used when a transaction cannot be executed
|
||||
// and hence all subsequent ones should be discarded from the same account.
|
||||
func (t *transactionsByPriceAndNonce) Pop() { |
||||
heap.Pop(&t.heads) |
||||
} |
@ -0,0 +1,188 @@ |
||||
// Copyright 2014 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package miner |
||||
|
||||
import ( |
||||
"crypto/ecdsa" |
||||
"math/big" |
||||
"math/rand" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core/txpool" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
) |
||||
|
||||
func TestTransactionPriceNonceSortLegacy(t *testing.T) { |
||||
testTransactionPriceNonceSort(t, nil) |
||||
} |
||||
|
||||
func TestTransactionPriceNonceSort1559(t *testing.T) { |
||||
testTransactionPriceNonceSort(t, big.NewInt(0)) |
||||
testTransactionPriceNonceSort(t, big.NewInt(5)) |
||||
testTransactionPriceNonceSort(t, big.NewInt(50)) |
||||
} |
||||
|
||||
// Tests that transactions can be correctly sorted according to their price in
|
||||
// decreasing order, but at the same time with increasing nonces when issued by
|
||||
// the same account.
|
||||
func testTransactionPriceNonceSort(t *testing.T, baseFee *big.Int) { |
||||
// Generate a batch of accounts to start with
|
||||
keys := make([]*ecdsa.PrivateKey, 25) |
||||
for i := 0; i < len(keys); i++ { |
||||
keys[i], _ = crypto.GenerateKey() |
||||
} |
||||
signer := types.LatestSignerForChainID(common.Big1) |
||||
|
||||
// Generate a batch of transactions with overlapping values, but shifted nonces
|
||||
groups := map[common.Address][]*txpool.LazyTransaction{} |
||||
expectedCount := 0 |
||||
for start, key := range keys { |
||||
addr := crypto.PubkeyToAddress(key.PublicKey) |
||||
count := 25 |
||||
for i := 0; i < 25; i++ { |
||||
var tx *types.Transaction |
||||
gasFeeCap := rand.Intn(50) |
||||
if baseFee == nil { |
||||
tx = types.NewTx(&types.LegacyTx{ |
||||
Nonce: uint64(start + i), |
||||
To: &common.Address{}, |
||||
Value: big.NewInt(100), |
||||
Gas: 100, |
||||
GasPrice: big.NewInt(int64(gasFeeCap)), |
||||
Data: nil, |
||||
}) |
||||
} else { |
||||
tx = types.NewTx(&types.DynamicFeeTx{ |
||||
Nonce: uint64(start + i), |
||||
To: &common.Address{}, |
||||
Value: big.NewInt(100), |
||||
Gas: 100, |
||||
GasFeeCap: big.NewInt(int64(gasFeeCap)), |
||||
GasTipCap: big.NewInt(int64(rand.Intn(gasFeeCap + 1))), |
||||
Data: nil, |
||||
}) |
||||
if count == 25 && int64(gasFeeCap) < baseFee.Int64() { |
||||
count = i |
||||
} |
||||
} |
||||
tx, err := types.SignTx(tx, signer, key) |
||||
if err != nil { |
||||
t.Fatalf("failed to sign tx: %s", err) |
||||
} |
||||
groups[addr] = append(groups[addr], &txpool.LazyTransaction{ |
||||
Hash: tx.Hash(), |
||||
Tx: &txpool.Transaction{Tx: tx}, |
||||
Time: tx.Time(), |
||||
GasFeeCap: tx.GasFeeCap(), |
||||
GasTipCap: tx.GasTipCap(), |
||||
}) |
||||
} |
||||
expectedCount += count |
||||
} |
||||
// Sort the transactions and cross check the nonce ordering
|
||||
txset := newTransactionsByPriceAndNonce(signer, groups, baseFee) |
||||
|
||||
txs := types.Transactions{} |
||||
for tx := txset.Peek(); tx != nil; tx = txset.Peek() { |
||||
txs = append(txs, tx.Tx.Tx) |
||||
txset.Shift() |
||||
} |
||||
if len(txs) != expectedCount { |
||||
t.Errorf("expected %d transactions, found %d", expectedCount, len(txs)) |
||||
} |
||||
for i, txi := range txs { |
||||
fromi, _ := types.Sender(signer, txi) |
||||
|
||||
// Make sure the nonce order is valid
|
||||
for j, txj := range txs[i+1:] { |
||||
fromj, _ := types.Sender(signer, txj) |
||||
if fromi == fromj && txi.Nonce() > txj.Nonce() { |
||||
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()) |
||||
} |
||||
} |
||||
// If the next tx has different from account, the price must be lower than the current one
|
||||
if i+1 < len(txs) { |
||||
next := txs[i+1] |
||||
fromNext, _ := types.Sender(signer, next) |
||||
tip, err := txi.EffectiveGasTip(baseFee) |
||||
nextTip, nextErr := next.EffectiveGasTip(baseFee) |
||||
if err != nil || nextErr != nil { |
||||
t.Errorf("error calculating effective tip: %v, %v", err, nextErr) |
||||
} |
||||
if fromi != fromNext && tip.Cmp(nextTip) < 0 { |
||||
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()) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Tests that if multiple transactions have the same price, the ones seen earlier
|
||||
// are prioritized to avoid network spam attacks aiming for a specific ordering.
|
||||
func TestTransactionTimeSort(t *testing.T) { |
||||
// Generate a batch of accounts to start with
|
||||
keys := make([]*ecdsa.PrivateKey, 5) |
||||
for i := 0; i < len(keys); i++ { |
||||
keys[i], _ = crypto.GenerateKey() |
||||
} |
||||
signer := types.HomesteadSigner{} |
||||
|
||||
// Generate a batch of transactions with overlapping prices, but different creation times
|
||||
groups := map[common.Address][]*txpool.LazyTransaction{} |
||||
for start, key := range keys { |
||||
addr := crypto.PubkeyToAddress(key.PublicKey) |
||||
|
||||
tx, _ := types.SignTx(types.NewTransaction(0, common.Address{}, big.NewInt(100), 100, big.NewInt(1), nil), signer, key) |
||||
tx.SetTime(time.Unix(0, int64(len(keys)-start))) |
||||
|
||||
groups[addr] = append(groups[addr], &txpool.LazyTransaction{ |
||||
Hash: tx.Hash(), |
||||
Tx: &txpool.Transaction{Tx: tx}, |
||||
Time: tx.Time(), |
||||
GasFeeCap: tx.GasFeeCap(), |
||||
GasTipCap: tx.GasTipCap(), |
||||
}) |
||||
} |
||||
// Sort the transactions and cross check the nonce ordering
|
||||
txset := newTransactionsByPriceAndNonce(signer, groups, nil) |
||||
|
||||
txs := types.Transactions{} |
||||
for tx := txset.Peek(); tx != nil; tx = txset.Peek() { |
||||
txs = append(txs, tx.Tx.Tx) |
||||
txset.Shift() |
||||
} |
||||
if len(txs) != len(keys) { |
||||
t.Errorf("expected %d transactions, found %d", len(keys), len(txs)) |
||||
} |
||||
for i, txi := range txs { |
||||
fromi, _ := types.Sender(signer, txi) |
||||
if i+1 < len(txs) { |
||||
next := txs[i+1] |
||||
fromNext, _ := types.Sender(signer, next) |
||||
|
||||
if txi.GasPrice().Cmp(next.GasPrice()) < 0 { |
||||
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()) |
||||
} |
||||
// Make sure time order is ascending if the txs have the same gas price
|
||||
if txi.GasPrice().Cmp(next.GasPrice()) == 0 && txi.Time().After(next.Time()) { |
||||
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()) |
||||
} |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue