// 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 . package blobpool import ( "bytes" "crypto/ecdsa" "crypto/sha256" "errors" "math" "math/big" "os" "path/filepath" "sync" "testing" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/ethdb/memorydb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/holiman/billy" "github.com/holiman/uint256" ) var ( emptyBlob = kzg4844.Blob{} emptyBlobCommit, _ = kzg4844.BlobToCommitment(emptyBlob) emptyBlobProof, _ = kzg4844.ComputeBlobProof(emptyBlob, emptyBlobCommit) emptyBlobVHash = kzg4844.CalcBlobHashV1(sha256.New(), &emptyBlobCommit) ) // Chain configuration with Cancun enabled. // // TODO(karalabe): replace with params.MainnetChainConfig after Cancun. var testChainConfig *params.ChainConfig func init() { testChainConfig = new(params.ChainConfig) *testChainConfig = *params.MainnetChainConfig testChainConfig.CancunTime = new(uint64) *testChainConfig.CancunTime = uint64(time.Now().Unix()) } // testBlockChain is a mock of the live chain for testing the pool. type testBlockChain struct { config *params.ChainConfig basefee *uint256.Int blobfee *uint256.Int statedb *state.StateDB } func (bc *testBlockChain) Config() *params.ChainConfig { return bc.config } func (bc *testBlockChain) CurrentBlock() *types.Header { // Yolo, life is too short to invert mist.CalcBaseFee and misc.CalcBlobFee, // just binary search it them. // The base fee at 5714 ETH translates into the 21000 base gas higher than // mainnet ether existence, use that as a cap for the tests. var ( blockNumber = new(big.Int).Add(bc.config.LondonBlock, big.NewInt(1)) blockTime = *bc.config.CancunTime + 1 gasLimit = uint64(30_000_000) ) lo := new(big.Int) hi := new(big.Int).Mul(big.NewInt(5714), new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil)) for new(big.Int).Add(lo, big.NewInt(1)).Cmp(hi) != 0 { mid := new(big.Int).Add(lo, hi) mid.Div(mid, big.NewInt(2)) if eip1559.CalcBaseFee(bc.config, &types.Header{ Number: blockNumber, GasLimit: gasLimit, GasUsed: 0, BaseFee: mid, }).Cmp(bc.basefee.ToBig()) > 0 { hi = mid } else { lo = mid } } baseFee := lo // The excess blob gas at 2^27 translates into a blob fee higher than mainnet // ether existence, use that as a cap for the tests. lo = new(big.Int) hi = new(big.Int).Exp(big.NewInt(2), big.NewInt(27), nil) for new(big.Int).Add(lo, big.NewInt(1)).Cmp(hi) != 0 { mid := new(big.Int).Add(lo, hi) mid.Div(mid, big.NewInt(2)) if eip4844.CalcBlobFee(mid.Uint64()).Cmp(bc.blobfee.ToBig()) > 0 { hi = mid } else { lo = mid } } excessBlobGas := lo.Uint64() return &types.Header{ Number: blockNumber, Time: blockTime, GasLimit: gasLimit, BaseFee: baseFee, ExcessBlobGas: &excessBlobGas, } } func (bc *testBlockChain) CurrentFinalBlock() *types.Header { return &types.Header{ Number: big.NewInt(0), } } func (bt *testBlockChain) GetBlock(hash common.Hash, number uint64) *types.Block { return nil } func (bc *testBlockChain) StateAt(common.Hash) (*state.StateDB, error) { return bc.statedb, nil } // makeAddressReserver is a utility method to sanity check that accounts are // properly reserved by the blobpool (no duplicate reserves or unreserves). func makeAddressReserver() txpool.AddressReserver { var ( reserved = make(map[common.Address]struct{}) lock sync.Mutex ) return func(addr common.Address, reserve bool) error { lock.Lock() defer lock.Unlock() _, exists := reserved[addr] if reserve { if exists { panic("already reserved") } reserved[addr] = struct{}{} return nil } if !exists { panic("not reserved") } delete(reserved, addr) return nil } } // makeTx is a utility method to construct a random blob transaction and sign it // with a valid key, only setting the interesting fields from the perspective of // the blob pool. func makeTx(nonce uint64, gasTipCap uint64, gasFeeCap uint64, blobFeeCap uint64, key *ecdsa.PrivateKey) *types.Transaction { blobtx := makeUnsignedTx(nonce, gasTipCap, gasFeeCap, blobFeeCap) return types.MustSignNewTx(key, types.LatestSigner(testChainConfig), blobtx) } // makeUnsignedTx is a utility method to construct a random blob tranasaction // without signing it. func makeUnsignedTx(nonce uint64, gasTipCap uint64, gasFeeCap uint64, blobFeeCap uint64) *types.BlobTx { return &types.BlobTx{ ChainID: uint256.MustFromBig(testChainConfig.ChainID), Nonce: nonce, GasTipCap: uint256.NewInt(gasTipCap), GasFeeCap: uint256.NewInt(gasFeeCap), Gas: 21000, BlobFeeCap: uint256.NewInt(blobFeeCap), BlobHashes: []common.Hash{emptyBlobVHash}, Value: uint256.NewInt(100), Sidecar: &types.BlobTxSidecar{ Blobs: []kzg4844.Blob{emptyBlob}, Commitments: []kzg4844.Commitment{emptyBlobCommit}, Proofs: []kzg4844.Proof{emptyBlobProof}, }, } } // verifyPoolInternals iterates over all the transactions in the pool and checks // that sort orders, calculated fields, cumulated fields are correct. func verifyPoolInternals(t *testing.T, pool *BlobPool) { // Mark this method as a helper to remove from stack traces t.Helper() // Verify that all items in the index are present in the lookup and nothing more seen := make(map[common.Hash]struct{}) for addr, txs := range pool.index { for _, tx := range txs { if _, ok := seen[tx.hash]; ok { t.Errorf("duplicate hash #%x in transaction index: address %s, nonce %d", tx.hash, addr, tx.nonce) } seen[tx.hash] = struct{}{} } } for hash, id := range pool.lookup { if _, ok := seen[hash]; !ok { t.Errorf("lookup entry missing from transaction index: hash #%x, id %d", hash, id) } delete(seen, hash) } for hash := range seen { t.Errorf("indexed transaction hash #%x missing from lookup table", hash) } // Verify that transactions are sorted per account and contain no nonce gaps for addr, txs := range pool.index { for i := 1; i < len(txs); i++ { if txs[i].nonce != txs[i-1].nonce+1 { t.Errorf("addr %v, tx %d nonce mismatch: have %d, want %d", addr, i, txs[i].nonce, txs[i-1].nonce+1) } } } // Verify that calculated evacuation thresholds are correct for addr, txs := range pool.index { if !txs[0].evictionExecTip.Eq(txs[0].execTipCap) { t.Errorf("addr %v, tx %d eviction execution tip mismatch: have %d, want %d", addr, 0, txs[0].evictionExecTip, txs[0].execTipCap) } if math.Abs(txs[0].evictionExecFeeJumps-txs[0].basefeeJumps) > 0.001 { t.Errorf("addr %v, tx %d eviction execution fee jumps mismatch: have %f, want %f", addr, 0, txs[0].evictionExecFeeJumps, txs[0].basefeeJumps) } if math.Abs(txs[0].evictionBlobFeeJumps-txs[0].blobfeeJumps) > 0.001 { t.Errorf("addr %v, tx %d eviction blob fee jumps mismatch: have %f, want %f", addr, 0, txs[0].evictionBlobFeeJumps, txs[0].blobfeeJumps) } for i := 1; i < len(txs); i++ { wantExecTip := txs[i-1].evictionExecTip if wantExecTip.Gt(txs[i].execTipCap) { wantExecTip = txs[i].execTipCap } if !txs[i].evictionExecTip.Eq(wantExecTip) { t.Errorf("addr %v, tx %d eviction execution tip mismatch: have %d, want %d", addr, i, txs[i].evictionExecTip, wantExecTip) } wantExecFeeJumps := txs[i-1].evictionExecFeeJumps if wantExecFeeJumps > txs[i].basefeeJumps { wantExecFeeJumps = txs[i].basefeeJumps } if math.Abs(txs[i].evictionExecFeeJumps-wantExecFeeJumps) > 0.001 { t.Errorf("addr %v, tx %d eviction execution fee jumps mismatch: have %f, want %f", addr, i, txs[i].evictionExecFeeJumps, wantExecFeeJumps) } wantBlobFeeJumps := txs[i-1].evictionBlobFeeJumps if wantBlobFeeJumps > txs[i].blobfeeJumps { wantBlobFeeJumps = txs[i].blobfeeJumps } if math.Abs(txs[i].evictionBlobFeeJumps-wantBlobFeeJumps) > 0.001 { t.Errorf("addr %v, tx %d eviction blob fee jumps mismatch: have %f, want %f", addr, i, txs[i].evictionBlobFeeJumps, wantBlobFeeJumps) } } } // Verify that account balance accumulations are correct for addr, txs := range pool.index { spent := new(uint256.Int) for _, tx := range txs { spent.Add(spent, tx.costCap) } if !pool.spent[addr].Eq(spent) { t.Errorf("addr %v expenditure mismatch: have %d, want %d", addr, pool.spent[addr], spent) } } // Verify that pool storage size is correct var stored uint64 for _, txs := range pool.index { for _, tx := range txs { stored += uint64(tx.size) } } if pool.stored != stored { t.Errorf("pool storage mismatch: have %d, want %d", pool.stored, stored) } // Verify the price heap internals verifyHeapInternals(t, pool.evict) } // Tests that transactions can be loaded from disk on startup and that they are // correctly discarded if invalid. // // - 1. A transaction that cannot be decoded must be dropped // - 2. A transaction that cannot be recovered (bad signature) must be dropped // - 3. All transactions after a nonce gap must be dropped // - 4. All transactions after an already included nonce must be dropped // - 5. All transactions after an underpriced one (including it) must be dropped // - 6. All transactions after an overdrafting sequence must be dropped // - 7. All transactions exceeding the per-account limit must be dropped // // Furthermore, some strange corner-cases can also occur after a crash, as Billy's // simplicity also allows it to resurrect past deleted entities: // // - 8. Fully duplicate transactions (matching hash) must be dropped // - 9. Duplicate nonces from the same account must be dropped func TestOpenDrops(t *testing.T) { log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelTrace, true))) // Create a temporary folder for the persistent backend storage, _ := os.MkdirTemp("", "blobpool-") defer os.RemoveAll(storage) os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700) store, _ := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotter(), nil) // Insert a malformed transaction to verify that decoding errors (or format // changes) are handled gracefully (case 1) malformed, _ := store.Put([]byte("this is a badly encoded transaction")) // Insert a transaction with a bad signature to verify that stale junk after // potential hard-forks can get evicted (case 2) tx := types.NewTx(&types.BlobTx{ ChainID: uint256.MustFromBig(testChainConfig.ChainID), GasTipCap: new(uint256.Int), GasFeeCap: new(uint256.Int), Gas: 0, Value: new(uint256.Int), Data: nil, BlobFeeCap: new(uint256.Int), V: new(uint256.Int), R: new(uint256.Int), S: new(uint256.Int), }) blob, _ := rlp.EncodeToBytes(tx) badsig, _ := store.Put(blob) // Insert a sequence of transactions with a nonce gap in between to verify // that anything gapped will get evicted (case 3). var ( gapper, _ = crypto.GenerateKey() valids = make(map[uint64]struct{}) gapped = make(map[uint64]struct{}) ) for _, nonce := range []uint64{0, 1, 3, 4, 6, 7} { // first gap at #2, another at #5 tx := makeTx(nonce, 1, 1, 1, gapper) blob, _ := rlp.EncodeToBytes(tx) id, _ := store.Put(blob) if nonce < 2 { valids[id] = struct{}{} } else { gapped[id] = struct{}{} } } // Insert a sequence of transactions with a gapped starting nonce to verify // that the entire set will get dropped (case 3). var ( dangler, _ = crypto.GenerateKey() dangling = make(map[uint64]struct{}) ) for _, nonce := range []uint64{1, 2, 3} { // first gap at #0, all set dangling tx := makeTx(nonce, 1, 1, 1, dangler) blob, _ := rlp.EncodeToBytes(tx) id, _ := store.Put(blob) dangling[id] = struct{}{} } // Insert a sequence of transactions with already passed nonces to veirfy // that the entire set will get dropped (case 4). var ( filler, _ = crypto.GenerateKey() filled = make(map[uint64]struct{}) ) for _, nonce := range []uint64{0, 1, 2} { // account nonce at 3, all set filled tx := makeTx(nonce, 1, 1, 1, filler) blob, _ := rlp.EncodeToBytes(tx) id, _ := store.Put(blob) filled[id] = struct{}{} } // Insert a sequence of transactions with partially passed nonces to veirfy // that the included part of the set will get dropped (case 4). var ( overlapper, _ = crypto.GenerateKey() overlapped = make(map[uint64]struct{}) ) for _, nonce := range []uint64{0, 1, 2, 3} { // account nonce at 2, half filled tx := makeTx(nonce, 1, 1, 1, overlapper) blob, _ := rlp.EncodeToBytes(tx) id, _ := store.Put(blob) if nonce >= 2 { valids[id] = struct{}{} } else { overlapped[id] = struct{}{} } } // Insert a sequence of transactions with an underpriced first to verify that // the entire set will get dropped (case 5). var ( underpayer, _ = crypto.GenerateKey() underpaid = make(map[uint64]struct{}) ) for i := 0; i < 5; i++ { // make #0 underpriced var tx *types.Transaction if i == 0 { tx = makeTx(uint64(i), 0, 0, 0, underpayer) } else { tx = makeTx(uint64(i), 1, 1, 1, underpayer) } blob, _ := rlp.EncodeToBytes(tx) id, _ := store.Put(blob) underpaid[id] = struct{}{} } // Insert a sequence of transactions with an underpriced in between to verify // that it and anything newly gapped will get evicted (case 5). var ( outpricer, _ = crypto.GenerateKey() outpriced = make(map[uint64]struct{}) ) for i := 0; i < 5; i++ { // make #2 underpriced var tx *types.Transaction if i == 2 { tx = makeTx(uint64(i), 0, 0, 0, outpricer) } else { tx = makeTx(uint64(i), 1, 1, 1, outpricer) } blob, _ := rlp.EncodeToBytes(tx) id, _ := store.Put(blob) if i < 2 { valids[id] = struct{}{} } else { outpriced[id] = struct{}{} } } // Insert a sequence of transactions fully overdrafted to verify that the // entire set will get invalidated (case 6). var ( exceeder, _ = crypto.GenerateKey() exceeded = make(map[uint64]struct{}) ) for _, nonce := range []uint64{0, 1, 2} { // nonce 0 overdrafts the account var tx *types.Transaction if nonce == 0 { tx = makeTx(nonce, 1, 100, 1, exceeder) } else { tx = makeTx(nonce, 1, 1, 1, exceeder) } blob, _ := rlp.EncodeToBytes(tx) id, _ := store.Put(blob) exceeded[id] = struct{}{} } // Insert a sequence of transactions partially overdrafted to verify that part // of the set will get invalidated (case 6). var ( overdrafter, _ = crypto.GenerateKey() overdrafted = make(map[uint64]struct{}) ) for _, nonce := range []uint64{0, 1, 2} { // nonce 1 overdrafts the account var tx *types.Transaction if nonce == 1 { tx = makeTx(nonce, 1, 100, 1, overdrafter) } else { tx = makeTx(nonce, 1, 1, 1, overdrafter) } blob, _ := rlp.EncodeToBytes(tx) id, _ := store.Put(blob) if nonce < 1 { valids[id] = struct{}{} } else { overdrafted[id] = struct{}{} } } // Insert a sequence of transactions overflowing the account cap to verify // that part of the set will get invalidated (case 7). var ( overcapper, _ = crypto.GenerateKey() overcapped = make(map[uint64]struct{}) ) for nonce := uint64(0); nonce < maxTxsPerAccount+3; nonce++ { blob, _ := rlp.EncodeToBytes(makeTx(nonce, 1, 1, 1, overcapper)) id, _ := store.Put(blob) if nonce < maxTxsPerAccount { valids[id] = struct{}{} } else { overcapped[id] = struct{}{} } } // Insert a batch of duplicated transactions to verify that only one of each // version will remain (case 8). var ( duplicater, _ = crypto.GenerateKey() duplicated = make(map[uint64]struct{}) ) for _, nonce := range []uint64{0, 1, 2} { blob, _ := rlp.EncodeToBytes(makeTx(nonce, 1, 1, 1, duplicater)) for i := 0; i < int(nonce)+1; i++ { id, _ := store.Put(blob) if i == 0 { valids[id] = struct{}{} } else { duplicated[id] = struct{}{} } } } // Insert a batch of duplicated nonces to verify that only one of each will // remain (case 9). var ( repeater, _ = crypto.GenerateKey() repeated = make(map[uint64]struct{}) ) for _, nonce := range []uint64{0, 1, 2} { for i := 0; i < int(nonce)+1; i++ { blob, _ := rlp.EncodeToBytes(makeTx(nonce, 1, uint64(i)+1 /* unique hashes */, 1, repeater)) id, _ := store.Put(blob) if i == 0 { valids[id] = struct{}{} } else { repeated[id] = struct{}{} } } } store.Close() // Create a blob pool out of the pre-seeded data statedb, _ := state.New(types.EmptyRootHash, state.NewDatabase(rawdb.NewDatabase(memorydb.New())), nil) statedb.AddBalance(crypto.PubkeyToAddress(gapper.PublicKey), uint256.NewInt(1000000)) statedb.AddBalance(crypto.PubkeyToAddress(dangler.PublicKey), uint256.NewInt(1000000)) statedb.AddBalance(crypto.PubkeyToAddress(filler.PublicKey), uint256.NewInt(1000000)) statedb.SetNonce(crypto.PubkeyToAddress(filler.PublicKey), 3) statedb.AddBalance(crypto.PubkeyToAddress(overlapper.PublicKey), uint256.NewInt(1000000)) statedb.SetNonce(crypto.PubkeyToAddress(overlapper.PublicKey), 2) statedb.AddBalance(crypto.PubkeyToAddress(underpayer.PublicKey), uint256.NewInt(1000000)) statedb.AddBalance(crypto.PubkeyToAddress(outpricer.PublicKey), uint256.NewInt(1000000)) statedb.AddBalance(crypto.PubkeyToAddress(exceeder.PublicKey), uint256.NewInt(1000000)) statedb.AddBalance(crypto.PubkeyToAddress(overdrafter.PublicKey), uint256.NewInt(1000000)) statedb.AddBalance(crypto.PubkeyToAddress(overcapper.PublicKey), uint256.NewInt(10000000)) statedb.AddBalance(crypto.PubkeyToAddress(duplicater.PublicKey), uint256.NewInt(1000000)) statedb.AddBalance(crypto.PubkeyToAddress(repeater.PublicKey), uint256.NewInt(1000000)) statedb.Commit(0, true) chain := &testBlockChain{ config: testChainConfig, basefee: uint256.NewInt(params.InitialBaseFee), blobfee: uint256.NewInt(params.BlobTxMinBlobGasprice), statedb: statedb, } pool := New(Config{Datadir: storage}, chain) if err := pool.Init(big.NewInt(1), chain.CurrentBlock(), makeAddressReserver()); err != nil { t.Fatalf("failed to create blob pool: %v", err) } defer pool.Close() // Verify that the malformed (case 1), badly signed (case 2) and gapped (case // 3) txs have been deleted from the pool alive := make(map[uint64]struct{}) for _, txs := range pool.index { for _, tx := range txs { switch tx.id { case malformed: t.Errorf("malformed RLP transaction remained in storage") case badsig: t.Errorf("invalidly signed transaction remained in storage") default: if _, ok := dangling[tx.id]; ok { t.Errorf("dangling transaction remained in storage: %d", tx.id) } else if _, ok := filled[tx.id]; ok { t.Errorf("filled transaction remained in storage: %d", tx.id) } else if _, ok := overlapped[tx.id]; ok { t.Errorf("overlapped transaction remained in storage: %d", tx.id) } else if _, ok := gapped[tx.id]; ok { t.Errorf("gapped transaction remained in storage: %d", tx.id) } else if _, ok := underpaid[tx.id]; ok { t.Errorf("underpaid transaction remained in storage: %d", tx.id) } else if _, ok := outpriced[tx.id]; ok { t.Errorf("outpriced transaction remained in storage: %d", tx.id) } else if _, ok := exceeded[tx.id]; ok { t.Errorf("fully overdrafted transaction remained in storage: %d", tx.id) } else if _, ok := overdrafted[tx.id]; ok { t.Errorf("partially overdrafted transaction remained in storage: %d", tx.id) } else if _, ok := overcapped[tx.id]; ok { t.Errorf("overcapped transaction remained in storage: %d", tx.id) } else if _, ok := duplicated[tx.id]; ok { t.Errorf("duplicated transaction remained in storage: %d", tx.id) } else if _, ok := repeated[tx.id]; ok { t.Errorf("repeated nonce transaction remained in storage: %d", tx.id) } else { alive[tx.id] = struct{}{} } } } } // Verify that the rest of the transactions remained alive if len(alive) != len(valids) { t.Errorf("valid transaction count mismatch: have %d, want %d", len(alive), len(valids)) } for id := range alive { if _, ok := valids[id]; !ok { t.Errorf("extra transaction %d", id) } } for id := range valids { if _, ok := alive[id]; !ok { t.Errorf("missing transaction %d", id) } } // Verify all the calculated pool internals. Interestingly, this is **not** // a duplication of the above checks, this actually validates the verifier // using the above already hard coded checks. // // Do not remove this, nor alter the above to be generic. verifyPoolInternals(t, pool) } // Tests that transactions loaded from disk are indexed correctly. // // - 1. Transactions must be grouped by sender, sorted by nonce // - 2. Eviction thresholds are calculated correctly for the sequences // - 3. Balance usage of an account is totals across all transactions func TestOpenIndex(t *testing.T) { log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelTrace, true))) // Create a temporary folder for the persistent backend storage, _ := os.MkdirTemp("", "blobpool-") defer os.RemoveAll(storage) os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700) store, _ := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotter(), nil) // Insert a sequence of transactions with varying price points to check that // the cumulative minimum will be maintained. var ( key, _ = crypto.GenerateKey() addr = crypto.PubkeyToAddress(key.PublicKey) txExecTipCaps = []uint64{10, 25, 5, 7, 1, 100} txExecFeeCaps = []uint64{100, 90, 200, 10, 80, 300} txBlobFeeCaps = []uint64{55, 66, 77, 33, 22, 11} //basefeeJumps = []float64{39.098, 38.204, 44.983, 19.549, 37.204, 48.426} // log 1.125 (exec fee cap) //blobfeeJumps = []float64{34.023, 35.570, 36.879, 29.686, 26.243, 20.358} // log 1.125 (blob fee cap) evictExecTipCaps = []uint64{10, 10, 5, 5, 1, 1} evictExecFeeJumps = []float64{39.098, 38.204, 38.204, 19.549, 19.549, 19.549} // min(log 1.125 (exec fee cap)) evictBlobFeeJumps = []float64{34.023, 34.023, 34.023, 29.686, 26.243, 20.358} // min(log 1.125 (blob fee cap)) totalSpent = uint256.NewInt(21000*(100+90+200+10+80+300) + blobSize*(55+66+77+33+22+11) + 100*6) // 21000 gas x price + 128KB x blobprice + value ) for _, i := range []int{5, 3, 4, 2, 0, 1} { // Randomize the tx insertion order to force sorting on load tx := makeTx(uint64(i), txExecTipCaps[i], txExecFeeCaps[i], txBlobFeeCaps[i], key) blob, _ := rlp.EncodeToBytes(tx) store.Put(blob) } store.Close() // Create a blob pool out of the pre-seeded data statedb, _ := state.New(types.EmptyRootHash, state.NewDatabase(rawdb.NewDatabase(memorydb.New())), nil) statedb.AddBalance(addr, uint256.NewInt(1_000_000_000)) statedb.Commit(0, true) chain := &testBlockChain{ config: testChainConfig, basefee: uint256.NewInt(params.InitialBaseFee), blobfee: uint256.NewInt(params.BlobTxMinBlobGasprice), statedb: statedb, } pool := New(Config{Datadir: storage}, chain) if err := pool.Init(big.NewInt(1), chain.CurrentBlock(), makeAddressReserver()); err != nil { t.Fatalf("failed to create blob pool: %v", err) } defer pool.Close() // Verify that the transactions have been sorted by nonce (case 1) for i := 0; i < len(pool.index[addr]); i++ { if pool.index[addr][i].nonce != uint64(i) { t.Errorf("tx %d nonce mismatch: have %d, want %d", i, pool.index[addr][i].nonce, uint64(i)) } } // Verify that the cumulative fee minimums have been correctly calculated (case 2) for i, cap := range evictExecTipCaps { if !pool.index[addr][i].evictionExecTip.Eq(uint256.NewInt(cap)) { t.Errorf("eviction tip cap %d mismatch: have %d, want %d", i, pool.index[addr][i].evictionExecTip, cap) } } for i, jumps := range evictExecFeeJumps { if math.Abs(pool.index[addr][i].evictionExecFeeJumps-jumps) > 0.001 { t.Errorf("eviction fee cap jumps %d mismatch: have %f, want %f", i, pool.index[addr][i].evictionExecFeeJumps, jumps) } } for i, jumps := range evictBlobFeeJumps { if math.Abs(pool.index[addr][i].evictionBlobFeeJumps-jumps) > 0.001 { t.Errorf("eviction blob fee cap jumps %d mismatch: have %f, want %f", i, pool.index[addr][i].evictionBlobFeeJumps, jumps) } } // Verify that the balance usage has been correctly calculated (case 3) if !pool.spent[addr].Eq(totalSpent) { t.Errorf("expenditure mismatch: have %d, want %d", pool.spent[addr], totalSpent) } // Verify all the calculated pool internals. Interestingly, this is **not** // a duplication of the above checks, this actually validates the verifier // using the above already hard coded checks. // // Do not remove this, nor alter the above to be generic. verifyPoolInternals(t, pool) } // Tests that after indexing all the loaded transactions from disk, a price heap // is correctly constructed based on the head basefee and blobfee. func TestOpenHeap(t *testing.T) { log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelTrace, true))) // Create a temporary folder for the persistent backend storage, _ := os.MkdirTemp("", "blobpool-") defer os.RemoveAll(storage) os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700) store, _ := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotter(), nil) // Insert a few transactions from a few accounts. To remove randomness from // the heap initialization, use a deterministic account/tx/priority ordering. var ( key1, _ = crypto.GenerateKey() key2, _ = crypto.GenerateKey() key3, _ = crypto.GenerateKey() addr1 = crypto.PubkeyToAddress(key1.PublicKey) addr2 = crypto.PubkeyToAddress(key2.PublicKey) addr3 = crypto.PubkeyToAddress(key3.PublicKey) ) if bytes.Compare(addr1[:], addr2[:]) > 0 { key1, addr1, key2, addr2 = key2, addr2, key1, addr1 } if bytes.Compare(addr1[:], addr3[:]) > 0 { key1, addr1, key3, addr3 = key3, addr3, key1, addr1 } if bytes.Compare(addr2[:], addr3[:]) > 0 { key2, addr2, key3, addr3 = key3, addr3, key2, addr2 } var ( tx1 = makeTx(0, 1, 1000, 90, key1) tx2 = makeTx(0, 1, 800, 70, key2) tx3 = makeTx(0, 1, 1500, 110, key3) blob1, _ = rlp.EncodeToBytes(tx1) blob2, _ = rlp.EncodeToBytes(tx2) blob3, _ = rlp.EncodeToBytes(tx3) heapOrder = []common.Address{addr2, addr1, addr3} heapIndex = map[common.Address]int{addr2: 0, addr1: 1, addr3: 2} ) store.Put(blob1) store.Put(blob2) store.Put(blob3) store.Close() // Create a blob pool out of the pre-seeded data statedb, _ := state.New(types.EmptyRootHash, state.NewDatabase(rawdb.NewDatabase(memorydb.New())), nil) statedb.AddBalance(addr1, uint256.NewInt(1_000_000_000)) statedb.AddBalance(addr2, uint256.NewInt(1_000_000_000)) statedb.AddBalance(addr3, uint256.NewInt(1_000_000_000)) statedb.Commit(0, true) chain := &testBlockChain{ config: testChainConfig, basefee: uint256.NewInt(1050), blobfee: uint256.NewInt(105), statedb: statedb, } pool := New(Config{Datadir: storage}, chain) if err := pool.Init(big.NewInt(1), chain.CurrentBlock(), makeAddressReserver()); err != nil { t.Fatalf("failed to create blob pool: %v", err) } defer pool.Close() // Verify that the heap's internal state matches the expectations for i, addr := range pool.evict.addrs { if addr != heapOrder[i] { t.Errorf("slot %d mismatch: have %v, want %v", i, addr, heapOrder[i]) } } for addr, i := range pool.evict.index { if i != heapIndex[addr] { t.Errorf("index for %v mismatch: have %d, want %d", addr, i, heapIndex[addr]) } } // Verify all the calculated pool internals. Interestingly, this is **not** // a duplication of the above checks, this actually validates the verifier // using the above already hard coded checks. // // Do not remove this, nor alter the above to be generic. verifyPoolInternals(t, pool) } // Tests that after the pool's previous state is loaded back, any transactions // over the new storage cap will get dropped. func TestOpenCap(t *testing.T) { log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelTrace, true))) // Create a temporary folder for the persistent backend storage, _ := os.MkdirTemp("", "blobpool-") defer os.RemoveAll(storage) os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700) store, _ := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotter(), nil) // Insert a few transactions from a few accounts var ( key1, _ = crypto.GenerateKey() key2, _ = crypto.GenerateKey() key3, _ = crypto.GenerateKey() addr1 = crypto.PubkeyToAddress(key1.PublicKey) addr2 = crypto.PubkeyToAddress(key2.PublicKey) addr3 = crypto.PubkeyToAddress(key3.PublicKey) tx1 = makeTx(0, 1, 1000, 100, key1) tx2 = makeTx(0, 1, 800, 70, key2) tx3 = makeTx(0, 1, 1500, 110, key3) blob1, _ = rlp.EncodeToBytes(tx1) blob2, _ = rlp.EncodeToBytes(tx2) blob3, _ = rlp.EncodeToBytes(tx3) keep = []common.Address{addr1, addr3} drop = []common.Address{addr2} size = uint64(2 * (txAvgSize + blobSize)) ) store.Put(blob1) store.Put(blob2) store.Put(blob3) store.Close() // Verify pool capping twice: first by reducing the data cap, then restarting // with a high cap to ensure everything was persisted previously for _, datacap := range []uint64{2 * (txAvgSize + blobSize), 100 * (txAvgSize + blobSize)} { // Create a blob pool out of the pre-seeded data, but cap it to 2 blob transaction statedb, _ := state.New(types.EmptyRootHash, state.NewDatabase(rawdb.NewDatabase(memorydb.New())), nil) statedb.AddBalance(addr1, uint256.NewInt(1_000_000_000)) statedb.AddBalance(addr2, uint256.NewInt(1_000_000_000)) statedb.AddBalance(addr3, uint256.NewInt(1_000_000_000)) statedb.Commit(0, true) chain := &testBlockChain{ config: testChainConfig, basefee: uint256.NewInt(1050), blobfee: uint256.NewInt(105), statedb: statedb, } pool := New(Config{Datadir: storage, Datacap: datacap}, chain) if err := pool.Init(big.NewInt(1), chain.CurrentBlock(), makeAddressReserver()); err != nil { t.Fatalf("failed to create blob pool: %v", err) } // Verify that enough transactions have been dropped to get the pool's size // under the requested limit if len(pool.index) != len(keep) { t.Errorf("tracked account count mismatch: have %d, want %d", len(pool.index), len(keep)) } for _, addr := range keep { if _, ok := pool.index[addr]; !ok { t.Errorf("expected account %v missing from pool", addr) } } for _, addr := range drop { if _, ok := pool.index[addr]; ok { t.Errorf("unexpected account %v present in pool", addr) } } if pool.stored != size { t.Errorf("pool stored size mismatch: have %v, want %v", pool.stored, size) } // Verify all the calculated pool internals. Interestingly, this is **not** // a duplication of the above checks, this actually validates the verifier // using the above already hard coded checks. // // Do not remove this, nor alter the above to be generic. verifyPoolInternals(t, pool) pool.Close() } } // Tests that adding transaction will correctly store it in the persistent store // and update all the indices. // // Note, this tests mostly checks the pool transaction shuffling logic or things // specific to the blob pool. It does not do an exhaustive transaction validity // check. func TestAdd(t *testing.T) { log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelTrace, true))) // seed is a helper tumpe to seed an initial state db and pool type seed struct { balance uint64 nonce uint64 txs []*types.BlobTx } // addtx is a helper sender/tx tuple to represent a new tx addition type addtx struct { from string tx *types.BlobTx err error } tests := []struct { seeds map[string]seed adds []addtx }{ // Transactions from new accounts should be accepted if their initial // nonce matches the expected one from the statedb. Higher or lower must // be rejected. { seeds: map[string]seed{ "alice": {balance: 21100 + blobSize}, "bob": {balance: 21100 + blobSize, nonce: 1}, "claire": {balance: 21100 + blobSize}, "dave": {balance: 21100 + blobSize, nonce: 1}, }, adds: []addtx{ { // New account, no previous txs: accept nonce 0 from: "alice", tx: makeUnsignedTx(0, 1, 1, 1), err: nil, }, { // Old account, 1 tx in chain, 0 pending: accept nonce 1 from: "bob", tx: makeUnsignedTx(1, 1, 1, 1), err: nil, }, { // New account, no previous txs: reject nonce 1 from: "claire", tx: makeUnsignedTx(1, 1, 1, 1), err: core.ErrNonceTooHigh, }, { // Old account, 1 tx in chain, 0 pending: reject nonce 0 from: "dave", tx: makeUnsignedTx(0, 1, 1, 1), err: core.ErrNonceTooLow, }, { // Old account, 1 tx in chain, 0 pending: reject nonce 2 from: "dave", tx: makeUnsignedTx(2, 1, 1, 1), err: core.ErrNonceTooHigh, }, }, }, // Transactions from already pooled accounts should only be accepted if // the nonces are contiguous (ignore prices for now, will check later) { seeds: map[string]seed{ "alice": { balance: 1000000, txs: []*types.BlobTx{ makeUnsignedTx(0, 1, 1, 1), }, }, "bob": { balance: 1000000, nonce: 1, txs: []*types.BlobTx{ makeUnsignedTx(1, 1, 1, 1), }, }, }, adds: []addtx{ { // New account, 1 tx pending: reject replacement nonce 0 (ignore price for now) from: "alice", tx: makeUnsignedTx(0, 1, 1, 1), err: txpool.ErrReplaceUnderpriced, }, { // New account, 1 tx pending: accept nonce 1 from: "alice", tx: makeUnsignedTx(1, 1, 1, 1), err: nil, }, { // New account, 2 txs pending: reject nonce 3 from: "alice", tx: makeUnsignedTx(3, 1, 1, 1), err: core.ErrNonceTooHigh, }, { // New account, 2 txs pending: accept nonce 2 from: "alice", tx: makeUnsignedTx(2, 1, 1, 1), err: nil, }, { // New account, 3 txs pending: accept nonce 3 now from: "alice", tx: makeUnsignedTx(3, 1, 1, 1), err: nil, }, { // Old account, 1 tx in chain, 1 tx pending: reject replacement nonce 1 (ignore price for now) from: "bob", tx: makeUnsignedTx(1, 1, 1, 1), err: txpool.ErrReplaceUnderpriced, }, { // Old account, 1 tx in chain, 1 tx pending: accept nonce 2 (ignore price for now) from: "bob", tx: makeUnsignedTx(2, 1, 1, 1), err: nil, }, }, }, // Transactions should only be accepted into the pool if the cumulative // expenditure doesn't overflow the account balance { seeds: map[string]seed{ "alice": {balance: 63299 + 3*blobSize}, // 3 tx - 1 wei }, adds: []addtx{ { // New account, no previous txs: accept nonce 0 with 21100 wei spend from: "alice", tx: makeUnsignedTx(0, 1, 1, 1), err: nil, }, { // New account, 1 pooled tx with 21100 wei spent: accept nonce 1 with 21100 wei spend from: "alice", tx: makeUnsignedTx(1, 1, 1, 1), err: nil, }, { // New account, 2 pooled tx with 42200 wei spent: reject nonce 2 with 21100 wei spend (1 wei overflow) from: "alice", tx: makeUnsignedTx(2, 1, 1, 1), err: core.ErrInsufficientFunds, }, }, }, // Transactions should only be accepted into the pool if the total count // from the same account doesn't overflow the pool limits { seeds: map[string]seed{ "alice": {balance: 10000000}, }, adds: []addtx{ { // New account, no previous txs, 16 slots left: accept nonce 0 from: "alice", tx: makeUnsignedTx(0, 1, 1, 1), err: nil, }, { // New account, 1 pooled tx, 15 slots left: accept nonce 1 from: "alice", tx: makeUnsignedTx(1, 1, 1, 1), err: nil, }, { // New account, 2 pooled tx, 14 slots left: accept nonce 2 from: "alice", tx: makeUnsignedTx(2, 1, 1, 1), err: nil, }, { // New account, 3 pooled tx, 13 slots left: accept nonce 3 from: "alice", tx: makeUnsignedTx(3, 1, 1, 1), err: nil, }, { // New account, 4 pooled tx, 12 slots left: accept nonce 4 from: "alice", tx: makeUnsignedTx(4, 1, 1, 1), err: nil, }, { // New account, 5 pooled tx, 11 slots left: accept nonce 5 from: "alice", tx: makeUnsignedTx(5, 1, 1, 1), err: nil, }, { // New account, 6 pooled tx, 10 slots left: accept nonce 6 from: "alice", tx: makeUnsignedTx(6, 1, 1, 1), err: nil, }, { // New account, 7 pooled tx, 9 slots left: accept nonce 7 from: "alice", tx: makeUnsignedTx(7, 1, 1, 1), err: nil, }, { // New account, 8 pooled tx, 8 slots left: accept nonce 8 from: "alice", tx: makeUnsignedTx(8, 1, 1, 1), err: nil, }, { // New account, 9 pooled tx, 7 slots left: accept nonce 9 from: "alice", tx: makeUnsignedTx(9, 1, 1, 1), err: nil, }, { // New account, 10 pooled tx, 6 slots left: accept nonce 10 from: "alice", tx: makeUnsignedTx(10, 1, 1, 1), err: nil, }, { // New account, 11 pooled tx, 5 slots left: accept nonce 11 from: "alice", tx: makeUnsignedTx(11, 1, 1, 1), err: nil, }, { // New account, 12 pooled tx, 4 slots left: accept nonce 12 from: "alice", tx: makeUnsignedTx(12, 1, 1, 1), err: nil, }, { // New account, 13 pooled tx, 3 slots left: accept nonce 13 from: "alice", tx: makeUnsignedTx(13, 1, 1, 1), err: nil, }, { // New account, 14 pooled tx, 2 slots left: accept nonce 14 from: "alice", tx: makeUnsignedTx(14, 1, 1, 1), err: nil, }, { // New account, 15 pooled tx, 1 slots left: accept nonce 15 from: "alice", tx: makeUnsignedTx(15, 1, 1, 1), err: nil, }, { // New account, 16 pooled tx, 0 slots left: accept nonce 15 replacement from: "alice", tx: makeUnsignedTx(15, 10, 10, 10), err: nil, }, { // New account, 16 pooled tx, 0 slots left: reject nonce 16 with overcap from: "alice", tx: makeUnsignedTx(16, 1, 1, 1), err: txpool.ErrAccountLimitExceeded, }, }, }, // Previously existing transactions should be allowed to be replaced iff // the new cumulative expenditure can be covered by the account and the // prices are bumped all around (no percentage check here). { seeds: map[string]seed{ "alice": {balance: 2*100 + 5*21000 + 3*blobSize}, }, adds: []addtx{ { // New account, no previous txs: reject nonce 0 with 341172 wei spend from: "alice", tx: makeUnsignedTx(0, 1, 20, 1), err: core.ErrInsufficientFunds, }, { // New account, no previous txs: accept nonce 0 with 173172 wei spend from: "alice", tx: makeUnsignedTx(0, 1, 2, 1), err: nil, }, { // New account, 1 pooled tx with 173172 wei spent: accept nonce 1 with 152172 wei spend from: "alice", tx: makeUnsignedTx(1, 1, 1, 1), err: nil, }, { // New account, 2 pooled tx with 325344 wei spent: reject nonce 0 with 599684 wei spend (173072 extra) (would overflow balance at nonce 1) from: "alice", tx: makeUnsignedTx(0, 2, 5, 2), err: core.ErrInsufficientFunds, }, { // New account, 2 pooled tx with 325344 wei spent: reject nonce 0 with no-gastip-bump from: "alice", tx: makeUnsignedTx(0, 1, 3, 2), err: txpool.ErrReplaceUnderpriced, }, { // New account, 2 pooled tx with 325344 wei spent: reject nonce 0 with no-gascap-bump from: "alice", tx: makeUnsignedTx(0, 2, 2, 2), err: txpool.ErrReplaceUnderpriced, }, { // New account, 2 pooled tx with 325344 wei spent: reject nonce 0 with no-blobcap-bump from: "alice", tx: makeUnsignedTx(0, 2, 4, 1), err: txpool.ErrReplaceUnderpriced, }, { // New account, 2 pooled tx with 325344 wei spent: accept nonce 0 with 84100 wei spend (42000 extra) from: "alice", tx: makeUnsignedTx(0, 2, 4, 2), err: nil, }, }, }, // Previously existing transactions should be allowed to be replaced iff // the new prices are bumped by a sufficient amount. { seeds: map[string]seed{ "alice": {balance: 100 + 8*21000 + 4*blobSize}, }, adds: []addtx{ { // New account, no previous txs: accept nonce 0 from: "alice", tx: makeUnsignedTx(0, 2, 4, 2), err: nil, }, { // New account, 1 pooled tx: reject nonce 0 with low-gastip-bump from: "alice", tx: makeUnsignedTx(0, 3, 8, 4), err: txpool.ErrReplaceUnderpriced, }, { // New account, 1 pooled tx: reject nonce 0 with low-gascap-bump from: "alice", tx: makeUnsignedTx(0, 4, 6, 4), err: txpool.ErrReplaceUnderpriced, }, { // New account, 1 pooled tx: reject nonce 0 with low-blobcap-bump from: "alice", tx: makeUnsignedTx(0, 4, 8, 3), err: txpool.ErrReplaceUnderpriced, }, { // New account, 1 pooled tx: accept nonce 0 with all-bumps from: "alice", tx: makeUnsignedTx(0, 4, 8, 4), err: nil, }, }, }, } for i, tt := range tests { // Create a temporary folder for the persistent backend storage, _ := os.MkdirTemp("", "blobpool-") defer os.RemoveAll(storage) // late defer, still ok os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700) store, _ := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotter(), nil) // Insert the seed transactions for the pool startup var ( keys = make(map[string]*ecdsa.PrivateKey) addrs = make(map[string]common.Address) ) statedb, _ := state.New(types.EmptyRootHash, state.NewDatabase(rawdb.NewDatabase(memorydb.New())), nil) for acc, seed := range tt.seeds { // Generate a new random key/address for the seed account keys[acc], _ = crypto.GenerateKey() addrs[acc] = crypto.PubkeyToAddress(keys[acc].PublicKey) // Seed the state database with this account statedb.AddBalance(addrs[acc], new(uint256.Int).SetUint64(seed.balance)) statedb.SetNonce(addrs[acc], seed.nonce) // Sign the seed transactions and store them in the data store for _, tx := range seed.txs { signed := types.MustSignNewTx(keys[acc], types.LatestSigner(testChainConfig), tx) blob, _ := rlp.EncodeToBytes(signed) store.Put(blob) } } statedb.Commit(0, true) store.Close() // Create a blob pool out of the pre-seeded dats chain := &testBlockChain{ config: testChainConfig, basefee: uint256.NewInt(1050), blobfee: uint256.NewInt(105), statedb: statedb, } pool := New(Config{Datadir: storage}, chain) if err := pool.Init(big.NewInt(1), chain.CurrentBlock(), makeAddressReserver()); err != nil { t.Fatalf("test %d: failed to create blob pool: %v", i, err) } verifyPoolInternals(t, pool) // Add each transaction one by one, verifying the pool internals in between for j, add := range tt.adds { signed, _ := types.SignNewTx(keys[add.from], types.LatestSigner(testChainConfig), add.tx) if err := pool.add(signed); !errors.Is(err, add.err) { t.Errorf("test %d, tx %d: adding transaction error mismatch: have %v, want %v", i, j, err, add.err) } verifyPoolInternals(t, pool) } // Verify the pool internals and close down the test verifyPoolInternals(t, pool) pool.Close() } }