txpool changes for eip-3860 (#820)

shanghai changes to txpool.

intrinsic gas updated to match the new version in Erigon.

unsure if the `isShanghai` check is robust enough or there would be a
better way to approach this.
This commit is contained in:
hexoscott 2023-01-16 22:15:42 +00:00 committed by GitHub
parent 61706714c3
commit 8969b9c58f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 286 additions and 26 deletions

View File

@ -154,4 +154,8 @@ const (
// up to half the consumed gas could be refunded. Redefined as 1/5th in EIP-3529
RefundQuotient uint64 = 2
RefundQuotientEIP3529 uint64 = 5
// EIP-3860 to limit size of initcode
MaxInitCodeSize = 2 * MaxCodeSize // Maximum initcode to permit in a creation transaction and create instructions
InitCodeWordGas = 2
)

17
common/math/integer.go Normal file
View File

@ -0,0 +1,17 @@
package math
import (
"math/bits"
)
// SafeMul returns x*y and checks for overflow.
func SafeMul(x, y uint64) (uint64, bool) {
hi, lo := bits.Mul64(x, y)
return lo, hi != 0
}
// SafeAdd returns x+y and checks for overflow.
func SafeAdd(x, y uint64) (uint64, bool) {
sum, carryOut := bits.Add64(x, y, 0)
return sum, carryOut != 0
}

View File

@ -25,6 +25,7 @@ import (
"encoding/json"
"fmt"
"math"
"math/big"
"runtime"
"sort"
"sync"
@ -44,6 +45,7 @@ import (
"github.com/ledgerwatch/erigon-lib/common/cmp"
"github.com/ledgerwatch/erigon-lib/common/dbg"
"github.com/ledgerwatch/erigon-lib/common/fixedgas"
emath "github.com/ledgerwatch/erigon-lib/common/math"
"github.com/ledgerwatch/erigon-lib/common/u256"
"github.com/ledgerwatch/erigon-lib/gointerfaces"
"github.com/ledgerwatch/erigon-lib/gointerfaces/grpcutil"
@ -83,6 +85,7 @@ type Config struct {
MinFeeCap uint64
AccountSlots uint64 // Number of executable transaction slots guaranteed per account
PriceBump uint64 // Price bump percentage to replace an already existing transaction
OverrideShanghaiTime *big.Int
}
var DefaultConfig = Config{
@ -95,9 +98,10 @@ var DefaultConfig = Config{
BaseFeeSubPoolLimit: 10_000,
QueuedSubPoolLimit: 10_000,
MinFeeCap: 1,
AccountSlots: 16, //TODO: to choose right value (16 to be compatible with Geth)
PriceBump: 10, // Price bump percentage to replace an already existing transaction
MinFeeCap: 1,
AccountSlots: 16, //TODO: to choose right value (16 to be compatible with Geth)
PriceBump: 10, // Price bump percentage to replace an already existing transaction
OverrideShanghaiTime: nil,
}
// Pool is interface for the transaction pool
@ -166,6 +170,7 @@ const (
InsufficientFunds DiscardReason = 19
NotReplaced DiscardReason = 20 // There was an existing transaction with the same sender and nonce, not enough price bump to replace
DuplicateHash DiscardReason = 21 // There was an existing transaction with the same hash
InitCodeTooLarge DiscardReason = 22 // EIP-3860 - transaction init code is too large
)
func (r DiscardReason) String() string {
@ -214,6 +219,8 @@ func (r DiscardReason) String() string {
return "could not replace existing tx"
case DuplicateHash:
return "existing tx with same hash"
case InitCodeTooLarge:
return "initcode too large"
default:
panic(fmt.Sprintf("discard reason: %d", r))
}
@ -320,9 +327,11 @@ type TxPool struct {
started atomic.Bool
pendingBaseFee atomic.Uint64
blockGasLimit atomic.Uint64
shanghaiTime *big.Int
isPostShanghai atomic.Bool
}
func New(newTxs chan types.Hashes, coreDB kv.RoDB, cfg Config, cache kvcache.Cache, chainID uint256.Int) (*TxPool, error) {
func New(newTxs chan types.Hashes, coreDB kv.RoDB, cfg Config, cache kvcache.Cache, chainID uint256.Int, shanghaiTime *big.Int) (*TxPool, error) {
localsHistory, err := simplelru.NewLRU(10_000, nil)
if err != nil {
return nil, err
@ -360,6 +369,7 @@ func New(newTxs chan types.Hashes, coreDB kv.RoDB, cfg Config, cache kvcache.Cac
unprocessedRemoteTxs: &types.TxSlots{},
unprocessedRemoteByHash: map[string]int{},
promoted: make(types.Hashes, 0, 32*1024),
shanghaiTime: shanghaiTime,
}, nil
}
@ -608,6 +618,8 @@ func (p *TxPool) best(n uint16, txs *types.TxsRlp, tx kv.Tx, onTopOf, availableG
return false, 0, nil // Too early
}
isShanghai := p.isShanghai()
txs.Resize(uint(cmp.Min(int(n), len(p.pending.best.ms))))
var toRemove []*metaTx
count := 0
@ -645,7 +657,7 @@ func (p *TxPool) best(n uint16, txs *types.TxsRlp, tx kv.Tx, onTopOf, availableG
// make sure we have enough gas in the caller to add this transaction.
// not an exact science using intrinsic gas but as close as we could hope for at
// this stage
intrinsicGas, _ := CalcIntrinsicGas(uint64(mt.Tx.DataLen), uint64(mt.Tx.DataNonZeroLen), nil, mt.Tx.Creation, true, true)
intrinsicGas, _ := CalcIntrinsicGas(uint64(mt.Tx.DataLen), uint64(mt.Tx.DataNonZeroLen), nil, mt.Tx.Creation, true, true, isShanghai)
if intrinsicGas > availableGas {
// we might find another TX with a low enough intrinsic gas to include so carry on
continue
@ -712,6 +724,13 @@ func (p *TxPool) AddRemoteTxs(_ context.Context, newTxs types.TxSlots) {
}
func (p *TxPool) validateTx(txn *types.TxSlot, isLocal bool, stateCache kvcache.CacheView) DiscardReason {
isShanghai := p.isShanghai()
if isShanghai {
if txn.DataLen > fixedgas.MaxInitCodeSize {
return InitCodeTooLarge
}
}
// Drop non-local transactions under our own minimal accepted gas price or tip
if !isLocal && uint256.NewInt(p.cfg.MinFeeCap).Cmp(&txn.FeeCap) == 1 {
if txn.Traced {
@ -719,7 +738,7 @@ func (p *TxPool) validateTx(txn *types.TxSlot, isLocal bool, stateCache kvcache.
}
return UnderPriced
}
gas, reason := CalcIntrinsicGas(uint64(txn.DataLen), uint64(txn.DataNonZeroLen), nil, txn.Creation, true, true)
gas, reason := CalcIntrinsicGas(uint64(txn.DataLen), uint64(txn.DataNonZeroLen), nil, txn.Creation, true, true, isShanghai)
if txn.Traced {
log.Info(fmt.Sprintf("TX TRACING: validateTx intrinsic gas idHash=%x gas=%d", txn.IDHash, gas))
}
@ -763,6 +782,31 @@ func (p *TxPool) validateTx(txn *types.TxSlot, isLocal bool, stateCache kvcache.
return Success
}
func (p *TxPool) isShanghai() bool {
// once this flag has been set for the first time we no longer need to check the timestamp
set := p.isPostShanghai.Load()
if set {
return true
}
if p.shanghaiTime == nil {
return false
}
shanghaiTime := p.shanghaiTime.Uint64()
// a zero here means shanghai is always active
if shanghaiTime == 0 {
p.isPostShanghai.Swap(true)
return true
}
now := big.NewInt(time.Now().Unix())
is := now.Uint64() >= shanghaiTime
if is {
p.isPostShanghai.Swap(true)
}
return is
}
func (p *TxPool) ValidateSerializedTxn(serializedTxn []byte) error {
const (
// txSlotSize is used to calculate how many data slots a single transaction
@ -1805,7 +1849,7 @@ func (p *TxPool) deprecatedForEach(_ context.Context, f func(rlp, sender []byte,
}
// CalcIntrinsicGas computes the 'intrinsic gas' for a message with the given data.
func CalcIntrinsicGas(dataLen, dataNonZeroLen uint64, accessList types.AccessList, isContractCreation, isHomestead, isEIP2028 bool) (uint64, DiscardReason) {
func CalcIntrinsicGas(dataLen, dataNonZeroLen uint64, accessList types.AccessList, isContractCreation, isHomestead, isEIP2028, isShanghai bool) (uint64, DiscardReason) {
// Set the starting gas for the raw transaction
var gas uint64
if isContractCreation && isHomestead {
@ -1822,24 +1866,69 @@ func CalcIntrinsicGas(dataLen, dataNonZeroLen uint64, accessList types.AccessLis
if isEIP2028 {
nonZeroGas = fixedgas.TxDataNonZeroGasEIP2028
}
if (math.MaxUint64-gas)/nonZeroGas < nz {
product, overflow := emath.SafeMul(nz, nonZeroGas)
if overflow {
return 0, GasUintOverflow
}
gas, overflow = emath.SafeAdd(gas, product)
if overflow {
return 0, GasUintOverflow
}
gas += nz * nonZeroGas
z := dataLen - nz
if (math.MaxUint64-gas)/fixedgas.TxDataZeroGas < z {
product, overflow = emath.SafeMul(z, fixedgas.TxDataZeroGas)
if overflow {
return 0, GasUintOverflow
}
gas += z * fixedgas.TxDataZeroGas
gas, overflow = emath.SafeAdd(gas, product)
if overflow {
return 0, GasUintOverflow
}
if isContractCreation && isShanghai {
numWords := toWordSize(dataLen)
product, overflow = emath.SafeMul(numWords, fixedgas.InitCodeWordGas)
if overflow {
return 0, GasUintOverflow
}
gas, overflow = emath.SafeAdd(gas, product)
if overflow {
return 0, GasUintOverflow
}
}
}
if accessList != nil {
gas += uint64(len(accessList)) * fixedgas.TxAccessListAddressGas
gas += uint64(accessList.StorageKeys()) * fixedgas.TxAccessListStorageKeyGas
product, overflow := emath.SafeMul(uint64(len(accessList)), fixedgas.TxAccessListAddressGas)
if overflow {
return 0, GasUintOverflow
}
gas, overflow = emath.SafeAdd(gas, product)
if overflow {
return 0, GasUintOverflow
}
product, overflow = emath.SafeMul(uint64(accessList.StorageKeys()), fixedgas.TxAccessListStorageKeyGas)
if overflow {
return 0, GasUintOverflow
}
gas, overflow = emath.SafeAdd(gas, product)
if overflow {
return 0, GasUintOverflow
}
}
return gas, Success
}
// toWordSize returns the ceiled word size required for memory expansion.
func toWordSize(size uint64) uint64 {
if size > math.MaxUint64-31 {
return math.MaxUint64/32 + 1
}
return (size + 31) / 32
}
var PoolChainConfigKey = []byte("chain_config")
var PoolLastSeenBlockKey = []byte("last_seen_block")
var PoolPendingBaseFeeKey = []byte("pending_base_fee")

View File

@ -9,6 +9,10 @@ import (
"testing"
"github.com/holiman/uint256"
"github.com/ledgerwatch/log/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ledgerwatch/erigon-lib/common/u256"
"github.com/ledgerwatch/erigon-lib/gointerfaces"
"github.com/ledgerwatch/erigon-lib/gointerfaces/remote"
@ -17,9 +21,6 @@ import (
"github.com/ledgerwatch/erigon-lib/kv/memdb"
"github.com/ledgerwatch/erigon-lib/rlp"
"github.com/ledgerwatch/erigon-lib/types"
"github.com/ledgerwatch/log/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// https://go.dev/doc/fuzz/
@ -310,7 +311,7 @@ func FuzzOnNewBlocks(f *testing.F) {
cfg := DefaultConfig
sendersCache := kvcache.New(kvcache.DefaultCoherentConfig)
pool, err := New(ch, coreDB, cfg, sendersCache, *u256.N1)
pool, err := New(ch, coreDB, cfg, sendersCache, *u256.N1, nil)
assert.NoError(err)
pool.senders.senderIDs = senderIDs
for addr, id := range senderIDs {
@ -542,7 +543,7 @@ func FuzzOnNewBlocks(f *testing.F) {
check(p2pReceived, types.TxSlots{}, "after_flush")
checkNotify(p2pReceived, types.TxSlots{}, "after_flush")
p2, err := New(ch, coreDB, DefaultConfig, sendersCache, *u256.N1)
p2, err := New(ch, coreDB, DefaultConfig, sendersCache, *u256.N1, nil)
assert.NoError(err)
p2.senders = pool.senders // senders are not persisted
err = coreDB.View(ctx, func(coreTx kv.Tx) error { return p2.fromDB(ctx, tx, coreTx) })

View File

@ -21,6 +21,8 @@ import (
"container/heap"
"context"
"fmt"
"math"
"math/big"
"math/rand"
"testing"
@ -30,9 +32,11 @@ import (
"github.com/ledgerwatch/erigon-lib/common"
"github.com/ledgerwatch/erigon-lib/common/cmp"
"github.com/ledgerwatch/erigon-lib/common/fixedgas"
"github.com/ledgerwatch/erigon-lib/common/u256"
"github.com/ledgerwatch/erigon-lib/gointerfaces"
"github.com/ledgerwatch/erigon-lib/gointerfaces/remote"
"github.com/ledgerwatch/erigon-lib/kv"
"github.com/ledgerwatch/erigon-lib/kv/kvcache"
"github.com/ledgerwatch/erigon-lib/kv/memdb"
"github.com/ledgerwatch/erigon-lib/types"
@ -94,7 +98,7 @@ func TestNonceFromAddress(t *testing.T) {
cfg := DefaultConfig
sendersCache := kvcache.New(kvcache.DefaultCoherentConfig)
pool, err := New(ch, coreDB, cfg, sendersCache, *u256.N1)
pool, err := New(ch, coreDB, cfg, sendersCache, *u256.N1, nil)
assert.NoError(err)
require.True(pool != nil)
ctx := context.Background()
@ -214,7 +218,7 @@ func TestReplaceWithHigherFee(t *testing.T) {
cfg := DefaultConfig
sendersCache := kvcache.New(kvcache.DefaultCoherentConfig)
pool, err := New(ch, coreDB, cfg, sendersCache, *u256.N1)
pool, err := New(ch, coreDB, cfg, sendersCache, *u256.N1, nil)
assert.NoError(err)
require.True(pool != nil)
ctx := context.Background()
@ -331,7 +335,7 @@ func TestReverseNonces(t *testing.T) {
cfg := DefaultConfig
sendersCache := kvcache.New(kvcache.DefaultCoherentConfig)
pool, err := New(ch, coreDB, cfg, sendersCache, *u256.N1)
pool, err := New(ch, coreDB, cfg, sendersCache, *u256.N1, nil)
assert.NoError(err)
require.True(pool != nil)
ctx := context.Background()
@ -455,7 +459,7 @@ func TestTxPoke(t *testing.T) {
cfg := DefaultConfig
sendersCache := kvcache.New(kvcache.DefaultCoherentConfig)
pool, err := New(ch, coreDB, cfg, sendersCache, *u256.N1)
pool, err := New(ch, coreDB, cfg, sendersCache, *u256.N1, nil)
assert.NoError(err)
require.True(pool != nil)
ctx := context.Background()
@ -618,3 +622,141 @@ func TestTxPoke(t *testing.T) {
default:
}
}
func TestShanghaiIntrinsicGas(t *testing.T) {
cases := map[string]struct {
expected uint64
dataLen uint64
dataNonZeroLen uint64
creation bool
isShanghai bool
}{
"simple no data": {
expected: 21000,
dataLen: 0,
dataNonZeroLen: 0,
creation: false,
isShanghai: false,
},
"simple with data": {
expected: 21512,
dataLen: 32,
dataNonZeroLen: 32,
creation: false,
isShanghai: false,
},
"creation with data no shanghai": {
expected: 53512,
dataLen: 32,
dataNonZeroLen: 32,
creation: true,
isShanghai: false,
},
"creation with single word and shanghai": {
expected: 53514, // additional gas for single word
dataLen: 32,
dataNonZeroLen: 32,
creation: true,
isShanghai: true,
},
"creation between word 1 and 2 and shanghai": {
expected: 53532, // additional gas for going into 2nd word although not filling it
dataLen: 33,
dataNonZeroLen: 33,
creation: true,
isShanghai: true,
},
}
for name, c := range cases {
t.Run(name, func(t *testing.T) {
gas, reason := CalcIntrinsicGas(c.dataLen, c.dataNonZeroLen, nil, c.creation, true, true, c.isShanghai)
if reason != Success {
t.Errorf("expected success but got reason %v", reason)
}
if gas != c.expected {
t.Errorf("expected %v but got %v", c.expected, gas)
}
})
}
}
func TestShanghaiValidateTx(t *testing.T) {
asrt := assert.New(t)
tests := map[string]struct {
expected DiscardReason
dataLen int
isShanghai bool
}{
"no shanghai": {
expected: Success,
dataLen: 32,
isShanghai: false,
},
"shanghai within bounds": {
expected: Success,
dataLen: 32,
isShanghai: true,
},
"shanghai exactly on bound": {
expected: Success,
dataLen: fixedgas.MaxInitCodeSize,
isShanghai: true,
},
"shanghai one over bound": {
expected: InitCodeTooLarge,
dataLen: fixedgas.MaxInitCodeSize + 1,
isShanghai: true,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
ch := make(chan types.Hashes, 100)
_, coreDB := memdb.NewTestPoolDB(t), memdb.NewTestDB(t)
cfg := DefaultConfig
var shanghaiTime *big.Int
if test.isShanghai {
shanghaiTime = big.NewInt(0)
}
cache := &kvcache.DummyCache{}
pool, err := New(ch, coreDB, cfg, cache, *u256.N1, shanghaiTime)
asrt.NoError(err)
ctx := context.Background()
tx, err := coreDB.BeginRw(ctx)
defer tx.Rollback()
asrt.NoError(err)
sndr := sender{nonce: 0, balance: *uint256.NewInt(math.MaxUint64)}
sndrBytes := make([]byte, types.EncodeSenderLengthForStorage(sndr.nonce, sndr.balance))
types.EncodeSender(sndr.nonce, sndr.balance, sndrBytes)
err = tx.Put(kv.PlainState, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, sndrBytes)
asrt.NoError(err)
txn := &types.TxSlot{
DataLen: test.dataLen,
FeeCap: *uint256.NewInt(21000),
Gas: 500000,
SenderID: 0,
Creation: true,
}
txns := types.TxSlots{
Txs: append([]*types.TxSlot{}, txn),
Senders: types.Addresses{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
}
err = pool.senders.registerNewSenders(&txns)
asrt.NoError(err)
view, err := cache.View(ctx, tx)
asrt.NoError(err)
reason := pool.validateTx(txn, false, view)
if reason != test.expected {
t.Errorf("expected %v, got %v", test.expected, reason)
}
})
}
}

View File

@ -240,7 +240,7 @@ func mapDiscardReasonToProto(reason DiscardReason) txpool_proto.ImportResult {
return txpool_proto.ImportResult_ALREADY_EXISTS
case UnderPriced, ReplaceUnderpriced, FeeTooLow:
return txpool_proto.ImportResult_FEE_TOO_LOW
case InvalidSender, NegativeValue, OversizedData:
case InvalidSender, NegativeValue, OversizedData, InitCodeTooLarge:
return txpool_proto.ImportResult_INVALID
default:
return txpool_proto.ImportResult_INTERNAL_ERROR

View File

@ -23,6 +23,9 @@ import (
"github.com/c2h5oh/datasize"
"github.com/holiman/uint256"
"github.com/ledgerwatch/log/v3"
mdbx2 "github.com/torquem-ch/mdbx-go/mdbx"
"github.com/ledgerwatch/erigon-lib/chain"
"github.com/ledgerwatch/erigon-lib/direct"
"github.com/ledgerwatch/erigon-lib/kv"
@ -30,8 +33,6 @@ import (
"github.com/ledgerwatch/erigon-lib/kv/mdbx"
"github.com/ledgerwatch/erigon-lib/txpool"
"github.com/ledgerwatch/erigon-lib/types"
"github.com/ledgerwatch/log/v3"
mdbx2 "github.com/torquem-ch/mdbx-go/mdbx"
)
func SaveChainConfigIfNeed(ctx context.Context, coreDB kv.RoDB, txPoolDB kv.RwDB, force bool) (cc *chain.Config, blockNum uint64, err error) {
@ -115,7 +116,13 @@ func AllComponents(ctx context.Context, cfg txpool.Config, cache kvcache.Cache,
}
chainID, _ := uint256.FromBig(chainConfig.ChainID)
txPool, err := txpool.New(newTxs, chainDB, cfg, cache, *chainID)
shanghaiTime := chainConfig.ShanghaiTime
if cfg.OverrideShanghaiTime != nil {
shanghaiTime = cfg.OverrideShanghaiTime
}
txPool, err := txpool.New(newTxs, chainDB, cfg, cache, *chainID, shanghaiTime)
if err != nil {
return nil, nil, nil, nil, nil, err
}