diff --git a/common/fixedgas/protocol.go b/common/fixedgas/protocol.go index 143ebbc28..603094add 100644 --- a/common/fixedgas/protocol.go +++ b/common/fixedgas/protocol.go @@ -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 ) diff --git a/common/math/integer.go b/common/math/integer.go new file mode 100644 index 000000000..acf3c6d07 --- /dev/null +++ b/common/math/integer.go @@ -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 +} diff --git a/txpool/pool.go b/txpool/pool.go index 19a29bba6..7dec14895 100644 --- a/txpool/pool.go +++ b/txpool/pool.go @@ -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") diff --git a/txpool/pool_fuzz_test.go b/txpool/pool_fuzz_test.go index f50555b92..33c4d2006 100644 --- a/txpool/pool_fuzz_test.go +++ b/txpool/pool_fuzz_test.go @@ -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) }) diff --git a/txpool/pool_test.go b/txpool/pool_test.go index d2fe3965d..8a7d81448 100644 --- a/txpool/pool_test.go +++ b/txpool/pool_test.go @@ -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) + } + }) + } +} diff --git a/txpool/txpool_grpc_server.go b/txpool/txpool_grpc_server.go index 9db127e73..fd12a9235 100644 --- a/txpool/txpool_grpc_server.go +++ b/txpool/txpool_grpc_server.go @@ -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 diff --git a/txpool/txpooluitl/all_components.go b/txpool/txpooluitl/all_components.go index 1f6653eb4..b65169f95 100644 --- a/txpool/txpooluitl/all_components.go +++ b/txpool/txpooluitl/all_components.go @@ -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 }