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 updates
This commit is contained in:
Péter Szilágyi
2023-07-27 13:45:35 +03:00
committed by GitHub
parent 37b952a4a2
commit 1662228ac6
35 changed files with 4665 additions and 363 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+50
View File
@@ -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
}
+146
View File
@@ -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
}
+320
View File
@@ -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))
}
}
+44
View File
@@ -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)
}
+258
View File
@@ -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
}
+78
View File
@@ -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))
)
+90
View File
@@ -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")
}
}
+87
View File
@@ -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])
}
}
+38
View File
@@ -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
}
}
+58
View File
@@ -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])
}
}
}