core/txpool/legacypool: add support for SetCode transactions (#31073)
The new SetCode transaction type introduces some additional complexity when handling the transaction pool. This complexity stems from two new account behaviors: 1. The balance and nonce of an account can change during regular transaction execution *when they have a deployed delegation*. 2. The nonce and code of an account can change without any EVM execution at all. This is the "set code" mechanism introduced by EIP-7702. The first issue has already been considered extensively during the design of ERC-4337, and we're relatively confident in the solution of simply limiting the number of in-flight pending transactions an account can have to one. This puts a reasonable bound on transaction cancellation. Normally to cancel, you would need to spend 21,000 gas. Now it's possible to cancel for around the cost of warming the account and sending value (`2,600+9,000=11,600`). So 50% cheaper. The second issue is more novel and needs further consideration. Since authorizations are not bound to a specific transaction, we cannot drop transactions with conflicting authorizations. Otherwise, it might be possible to cherry-pick authorizations from txs and front run them with different txs at much lower fee amounts, effectively DoSing the authority. Fortunately, conflicting authorizations do not affect the underlying validity of the transaction so we can just accept both. --------- Co-authored-by: Marius van der Wijden <m.vanderwijden@live.de> Co-authored-by: Felix Lange <fjl@twurst.com>
This commit is contained in:
parent
22b9354494
commit
cdb66c89d6
|
@ -142,6 +142,7 @@ func (bc *testBlockChain) CurrentBlock() *types.Header {
|
|||
GasLimit: gasLimit,
|
||||
BaseFee: baseFee,
|
||||
ExcessBlobGas: &excessBlobGas,
|
||||
Difficulty: common.Big0,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1565,8 +1566,9 @@ func TestAdd(t *testing.T) {
|
|||
if tt.block != nil {
|
||||
// Fake a header for the new set of transactions
|
||||
header := &types.Header{
|
||||
Number: big.NewInt(int64(chain.CurrentBlock().Number.Uint64() + 1)),
|
||||
BaseFee: chain.CurrentBlock().BaseFee, // invalid, but nothing checks it, yolo
|
||||
Number: big.NewInt(int64(chain.CurrentBlock().Number.Uint64() + 1)),
|
||||
Difficulty: common.Big0,
|
||||
BaseFee: chain.CurrentBlock().BaseFee, // invalid, but nothing checks it, yolo
|
||||
}
|
||||
// Inject the fake block into the chain
|
||||
txs := make([]*types.Transaction, len(tt.block))
|
||||
|
|
|
@ -60,4 +60,13 @@ var (
|
|||
// input transaction of non-blob type when a blob transaction from this sender
|
||||
// remains pending (and vice-versa).
|
||||
ErrAlreadyReserved = errors.New("address already reserved")
|
||||
|
||||
// ErrAuthorityReserved is returned if a transaction has an authorization
|
||||
// signed by an address which already has in-flight transactions known to the
|
||||
// pool.
|
||||
ErrAuthorityReserved = errors.New("authority already reserved")
|
||||
|
||||
// ErrAuthorityNonce is returned if a transaction has an authorization with
|
||||
// a nonce that is not currently valid for the authority.
|
||||
ErrAuthorityNonceTooLow = errors.New("authority nonce too low")
|
||||
)
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"errors"
|
||||
"math"
|
||||
"math/big"
|
||||
"slices"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
@ -196,6 +197,20 @@ func (config *Config) sanitize() Config {
|
|||
// The pool separates processable transactions (which can be applied to the
|
||||
// current state) and future transactions. Transactions move between those
|
||||
// two states over time as they are received and processed.
|
||||
//
|
||||
// In addition to tracking transactions, the pool also tracks a set of pending SetCode
|
||||
// authorizations (EIP7702). This helps minimize number of transactions that can be
|
||||
// trivially churned in the pool. As a standard rule, any account with a deployed
|
||||
// delegation or an in-flight authorization to deploy a delegation will only be allowed a
|
||||
// single transaction slot instead of the standard number. This is due to the possibility
|
||||
// of the account being sweeped by an unrelated account.
|
||||
//
|
||||
// Because SetCode transactions can have many authorizations included, we avoid explicitly
|
||||
// checking their validity to save the state lookup. So long as the encompassing
|
||||
// transaction is valid, the authorization will be accepted and tracked by the pool. In
|
||||
// case the pool is tracking a pending / queued transaction from a specific account, it
|
||||
// will reject new transactions with delegations from that account with standard in-flight
|
||||
// transactions.
|
||||
type LegacyPool struct {
|
||||
config Config
|
||||
chainconfig *params.ChainConfig
|
||||
|
@ -263,7 +278,7 @@ func New(config Config, chain BlockChain) *LegacyPool {
|
|||
// pool, specifically, whether it is a Legacy, AccessList or Dynamic transaction.
|
||||
func (pool *LegacyPool) Filter(tx *types.Transaction) bool {
|
||||
switch tx.Type() {
|
||||
case types.LegacyTxType, types.AccessListTxType, types.DynamicFeeTxType:
|
||||
case types.LegacyTxType, types.AccessListTxType, types.DynamicFeeTxType, types.SetCodeTxType:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
@ -540,7 +555,8 @@ func (pool *LegacyPool) validateTxBasics(tx *types.Transaction) error {
|
|||
Accept: 0 |
|
||||
1<<types.LegacyTxType |
|
||||
1<<types.AccessListTxType |
|
||||
1<<types.DynamicFeeTxType,
|
||||
1<<types.DynamicFeeTxType |
|
||||
1<<types.SetCodeTxType,
|
||||
MaxSize: txMaxSize,
|
||||
MinTip: pool.gasTip.Load().ToBig(),
|
||||
}
|
||||
|
@ -565,6 +581,11 @@ func (pool *LegacyPool) validateTx(tx *types.Transaction) error {
|
|||
if list := pool.queue[addr]; list != nil {
|
||||
have += list.Len()
|
||||
}
|
||||
if pool.currentState.GetCodeHash(addr) != types.EmptyCodeHash || len(pool.all.auths[addr]) != 0 {
|
||||
// Allow at most one in-flight tx for delegated accounts or those with
|
||||
// a pending authorization.
|
||||
return have, max(0, 1-have)
|
||||
}
|
||||
return have, math.MaxInt
|
||||
},
|
||||
ExistingExpenditure: func(addr common.Address) *big.Int {
|
||||
|
@ -581,6 +602,18 @@ func (pool *LegacyPool) validateTx(tx *types.Transaction) error {
|
|||
}
|
||||
return nil
|
||||
},
|
||||
KnownConflicts: func(from common.Address, auths []common.Address) []common.Address {
|
||||
var conflicts []common.Address
|
||||
// Authorities cannot conflict with any pending or queued transactions.
|
||||
for _, addr := range auths {
|
||||
if list := pool.pending[addr]; list != nil {
|
||||
conflicts = append(conflicts, addr)
|
||||
} else if list := pool.queue[addr]; list != nil {
|
||||
conflicts = append(conflicts, addr)
|
||||
}
|
||||
}
|
||||
return conflicts
|
||||
},
|
||||
}
|
||||
if err := txpool.ValidateTransactionWithState(tx, pool.signer, opts); err != nil {
|
||||
return err
|
||||
|
@ -1334,15 +1367,13 @@ func (pool *LegacyPool) promoteExecutables(accounts []common.Address) []*types.T
|
|||
// Drop all transactions that are deemed too old (low nonce)
|
||||
forwards := list.Forward(pool.currentState.GetNonce(addr))
|
||||
for _, tx := range forwards {
|
||||
hash := tx.Hash()
|
||||
pool.all.Remove(hash)
|
||||
pool.all.Remove(tx.Hash())
|
||||
}
|
||||
log.Trace("Removed old queued transactions", "count", len(forwards))
|
||||
// Drop all transactions that are too costly (low balance or out of gas)
|
||||
drops, _ := list.Filter(pool.currentState.GetBalance(addr), gasLimit)
|
||||
for _, tx := range drops {
|
||||
hash := tx.Hash()
|
||||
pool.all.Remove(hash)
|
||||
pool.all.Remove(tx.Hash())
|
||||
}
|
||||
log.Trace("Removed unpayable queued transactions", "count", len(drops))
|
||||
queuedNofundsMeter.Mark(int64(len(drops)))
|
||||
|
@ -1531,8 +1562,8 @@ func (pool *LegacyPool) demoteUnexecutables() {
|
|||
drops, invalids := list.Filter(pool.currentState.GetBalance(addr), gasLimit)
|
||||
for _, tx := range drops {
|
||||
hash := tx.Hash()
|
||||
log.Trace("Removed unpayable pending transaction", "hash", hash)
|
||||
pool.all.Remove(hash)
|
||||
log.Trace("Removed unpayable pending transaction", "hash", hash)
|
||||
}
|
||||
pendingNofundsMeter.Mark(int64(len(drops)))
|
||||
|
||||
|
@ -1641,12 +1672,15 @@ type lookup struct {
|
|||
slots int
|
||||
lock sync.RWMutex
|
||||
txs map[common.Hash]*types.Transaction
|
||||
|
||||
auths map[common.Address][]common.Hash // All accounts with a pooled authorization
|
||||
}
|
||||
|
||||
// newLookup returns a new lookup structure.
|
||||
func newLookup() *lookup {
|
||||
return &lookup{
|
||||
txs: make(map[common.Hash]*types.Transaction),
|
||||
txs: make(map[common.Hash]*types.Transaction),
|
||||
auths: make(map[common.Address][]common.Hash),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1697,6 +1731,7 @@ func (t *lookup) Add(tx *types.Transaction) {
|
|||
slotsGauge.Update(int64(t.slots))
|
||||
|
||||
t.txs[tx.Hash()] = tx
|
||||
t.addAuthorities(tx)
|
||||
}
|
||||
|
||||
// Remove removes a transaction from the lookup.
|
||||
|
@ -1704,6 +1739,7 @@ func (t *lookup) Remove(hash common.Hash) {
|
|||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
|
||||
t.removeAuthorities(hash)
|
||||
tx, ok := t.txs[hash]
|
||||
if !ok {
|
||||
log.Error("No transaction found to be deleted", "hash", hash)
|
||||
|
@ -1727,6 +1763,43 @@ func (t *lookup) TxsBelowTip(threshold *big.Int) types.Transactions {
|
|||
return found
|
||||
}
|
||||
|
||||
// addAuthorities tracks the supplied tx in relation to each authority it
|
||||
// specifies.
|
||||
func (t *lookup) addAuthorities(tx *types.Transaction) {
|
||||
for _, addr := range tx.SetCodeAuthorities() {
|
||||
list, ok := t.auths[addr]
|
||||
if !ok {
|
||||
list = []common.Hash{}
|
||||
}
|
||||
if slices.Contains(list, tx.Hash()) {
|
||||
// Don't add duplicates.
|
||||
continue
|
||||
}
|
||||
list = append(list, tx.Hash())
|
||||
t.auths[addr] = list
|
||||
}
|
||||
}
|
||||
|
||||
// removeAuthorities stops tracking the supplied tx in relation to its
|
||||
// authorities.
|
||||
func (t *lookup) removeAuthorities(hash common.Hash) {
|
||||
for addr := range t.auths {
|
||||
list := t.auths[addr]
|
||||
// Remove tx from tracker.
|
||||
if i := slices.Index(list, hash); i >= 0 {
|
||||
list = append(list[:i], list[i+1:]...)
|
||||
} else {
|
||||
log.Error("Authority with untracked tx", "addr", addr, "hash", hash)
|
||||
}
|
||||
if len(list) == 0 {
|
||||
// If list is newly empty, delete it entirely.
|
||||
delete(t.auths, addr)
|
||||
continue
|
||||
}
|
||||
t.auths[addr] = list
|
||||
}
|
||||
}
|
||||
|
||||
// numSlots calculates the number of slots needed for a single transaction.
|
||||
func numSlots(tx *types.Transaction) int {
|
||||
return int((tx.Size() + txSlotSize - 1) / txSlotSize)
|
||||
|
|
|
@ -34,6 +34,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/core/tracing"
|
||||
"github.com/ethereum/go-ethereum/core/txpool"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
|
@ -79,8 +80,9 @@ func (bc *testBlockChain) Config() *params.ChainConfig {
|
|||
|
||||
func (bc *testBlockChain) CurrentBlock() *types.Header {
|
||||
return &types.Header{
|
||||
Number: new(big.Int),
|
||||
GasLimit: bc.gasLimit.Load(),
|
||||
Number: new(big.Int),
|
||||
Difficulty: common.Big0,
|
||||
GasLimit: bc.gasLimit.Load(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -128,6 +130,39 @@ func dynamicFeeTx(nonce uint64, gaslimit uint64, gasFee *big.Int, tip *big.Int,
|
|||
return tx
|
||||
}
|
||||
|
||||
type unsignedAuth struct {
|
||||
nonce uint64
|
||||
key *ecdsa.PrivateKey
|
||||
}
|
||||
|
||||
func setCodeTx(nonce uint64, key *ecdsa.PrivateKey, unsigned []unsignedAuth) *types.Transaction {
|
||||
return pricedSetCodeTx(nonce, 250000, uint256.NewInt(1000), uint256.NewInt(1), key, unsigned)
|
||||
}
|
||||
|
||||
func pricedSetCodeTx(nonce uint64, gaslimit uint64, gasFee, tip *uint256.Int, key *ecdsa.PrivateKey, unsigned []unsignedAuth) *types.Transaction {
|
||||
var authList []types.SetCodeAuthorization
|
||||
for _, u := range unsigned {
|
||||
auth, _ := types.SignSetCode(u.key, types.SetCodeAuthorization{
|
||||
ChainID: *uint256.MustFromBig(params.TestChainConfig.ChainID),
|
||||
Address: common.Address{0x42},
|
||||
Nonce: u.nonce,
|
||||
})
|
||||
authList = append(authList, auth)
|
||||
}
|
||||
return types.MustSignNewTx(key, types.LatestSignerForChainID(params.TestChainConfig.ChainID), &types.SetCodeTx{
|
||||
ChainID: uint256.MustFromBig(params.TestChainConfig.ChainID),
|
||||
Nonce: nonce,
|
||||
GasTipCap: tip,
|
||||
GasFeeCap: gasFee,
|
||||
Gas: gaslimit,
|
||||
To: common.Address{},
|
||||
Value: uint256.NewInt(100),
|
||||
Data: nil,
|
||||
AccessList: nil,
|
||||
AuthList: authList,
|
||||
})
|
||||
}
|
||||
|
||||
func makeAddressReserver() txpool.AddressReserver {
|
||||
var (
|
||||
reserved = make(map[common.Address]struct{})
|
||||
|
@ -2163,6 +2198,201 @@ func TestSlotCount(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestSetCodeTransactions tests a few scenarios regarding the EIP-7702
|
||||
// SetCodeTx.
|
||||
func TestSetCodeTransactions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create the pool to test the status retrievals with
|
||||
statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
|
||||
blockchain := newTestBlockChain(params.MergedTestChainConfig, 1000000, statedb, new(event.Feed))
|
||||
|
||||
pool := New(testTxPoolConfig, blockchain)
|
||||
pool.Init(testTxPoolConfig.PriceLimit, blockchain.CurrentBlock(), makeAddressReserver())
|
||||
defer pool.Close()
|
||||
|
||||
// Create the test accounts
|
||||
var (
|
||||
keyA, _ = crypto.GenerateKey()
|
||||
keyB, _ = crypto.GenerateKey()
|
||||
keyC, _ = crypto.GenerateKey()
|
||||
addrA = crypto.PubkeyToAddress(keyA.PublicKey)
|
||||
addrB = crypto.PubkeyToAddress(keyB.PublicKey)
|
||||
addrC = crypto.PubkeyToAddress(keyC.PublicKey)
|
||||
)
|
||||
testAddBalance(pool, addrA, big.NewInt(params.Ether))
|
||||
testAddBalance(pool, addrB, big.NewInt(params.Ether))
|
||||
testAddBalance(pool, addrC, big.NewInt(params.Ether))
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
pending int
|
||||
queued int
|
||||
run func(string)
|
||||
}{
|
||||
{
|
||||
// Check that only one in-flight transaction is allowed for accounts
|
||||
// with delegation set. Also verify the accepted transaction can be
|
||||
// replaced by fee.
|
||||
name: "only-one-in-flight",
|
||||
pending: 1,
|
||||
run: func(name string) {
|
||||
aa := common.Address{0xaa, 0xaa}
|
||||
statedb.SetCode(addrA, append(types.DelegationPrefix, aa.Bytes()...))
|
||||
statedb.SetCode(aa, []byte{byte(vm.ADDRESS), byte(vm.PUSH0), byte(vm.SSTORE)})
|
||||
// Send transactions. First is accepted, second is rejected.
|
||||
if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1), keyA)); err != nil {
|
||||
t.Fatalf("%s: failed to add remote transaction: %v", name, err)
|
||||
}
|
||||
if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1), keyA)); !errors.Is(err, txpool.ErrAccountLimitExceeded) {
|
||||
t.Fatalf("%s: error mismatch: want %v, have %v", name, txpool.ErrAccountLimitExceeded, err)
|
||||
}
|
||||
// Also check gapped transaction.
|
||||
if err := pool.addRemoteSync(pricedTransaction(2, 100000, big.NewInt(1), keyA)); !errors.Is(err, txpool.ErrAccountLimitExceeded) {
|
||||
t.Fatalf("%s: error mismatch: want %v, have %v", name, txpool.ErrAccountLimitExceeded, err)
|
||||
}
|
||||
// Replace by fee.
|
||||
if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(10), keyA)); err != nil {
|
||||
t.Fatalf("%s: failed to replace with remote transaction: %v", name, err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "allow-setcode-tx-with-pending-authority-tx",
|
||||
pending: 2,
|
||||
run: func(name string) {
|
||||
// Send two transactions where the first has no conflicting delegations and
|
||||
// the second should be allowed despite conflicting with the authorities in 1).
|
||||
if err := pool.addRemoteSync(setCodeTx(0, keyA, []unsignedAuth{{1, keyC}})); err != nil {
|
||||
t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err)
|
||||
}
|
||||
if err := pool.addRemoteSync(setCodeTx(0, keyB, []unsignedAuth{{1, keyC}})); err != nil {
|
||||
t.Fatalf("%s: failed to add conflicting delegation: %v", name, err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "allow-one-tx-from-pooled-delegation",
|
||||
pending: 2,
|
||||
run: func(name string) {
|
||||
// Verify C cannot originate another transaction when it has a pooled delegation.
|
||||
if err := pool.addRemoteSync(setCodeTx(0, keyA, []unsignedAuth{{0, keyC}})); err != nil {
|
||||
t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err)
|
||||
}
|
||||
if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1), keyC)); err != nil {
|
||||
t.Fatalf("%s: failed to add with pending delegatio: %v", name, err)
|
||||
}
|
||||
// Also check gapped transaction is rejected.
|
||||
if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1), keyC)); !errors.Is(err, txpool.ErrAccountLimitExceeded) {
|
||||
t.Fatalf("%s: error mismatch: want %v, have %v", name, txpool.ErrAccountLimitExceeded, err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "replace-by-fee-setcode-tx",
|
||||
pending: 1,
|
||||
run: func(name string) {
|
||||
// 4. Fee bump the setcode tx send.
|
||||
if err := pool.addRemoteSync(setCodeTx(0, keyB, []unsignedAuth{{1, keyC}})); err != nil {
|
||||
t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err)
|
||||
}
|
||||
if err := pool.addRemoteSync(pricedSetCodeTx(0, 250000, uint256.NewInt(2000), uint256.NewInt(2), keyB, []unsignedAuth{{0, keyC}})); err != nil {
|
||||
t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "allow-tx-from-replaced-authority",
|
||||
pending: 2,
|
||||
run: func(name string) {
|
||||
// Fee bump with a different auth list. Make sure that unlocks the authorities.
|
||||
if err := pool.addRemoteSync(pricedSetCodeTx(0, 250000, uint256.NewInt(10), uint256.NewInt(3), keyA, []unsignedAuth{{0, keyB}})); err != nil {
|
||||
t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err)
|
||||
}
|
||||
if err := pool.addRemoteSync(pricedSetCodeTx(0, 250000, uint256.NewInt(3000), uint256.NewInt(300), keyA, []unsignedAuth{{0, keyC}})); err != nil {
|
||||
t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err)
|
||||
}
|
||||
// Now send a regular tx from B.
|
||||
if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(10), keyB)); err != nil {
|
||||
t.Fatalf("%s: failed to replace with remote transaction: %v", name, err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "allow-tx-from-replaced-self-sponsor-authority",
|
||||
pending: 2,
|
||||
run: func(name string) {
|
||||
//
|
||||
if err := pool.addRemoteSync(pricedSetCodeTx(0, 250000, uint256.NewInt(10), uint256.NewInt(3), keyA, []unsignedAuth{{0, keyA}})); err != nil {
|
||||
t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err)
|
||||
}
|
||||
if err := pool.addRemoteSync(pricedSetCodeTx(0, 250000, uint256.NewInt(30), uint256.NewInt(30), keyA, []unsignedAuth{{0, keyB}})); err != nil {
|
||||
t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err)
|
||||
}
|
||||
// Now send a regular tx from keyA.
|
||||
if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1000), keyA)); err != nil {
|
||||
t.Fatalf("%s: failed to replace with remote transaction: %v", name, err)
|
||||
}
|
||||
// Make sure we can still send from keyB.
|
||||
if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1000), keyB)); err != nil {
|
||||
t.Fatalf("%s: failed to replace with remote transaction: %v", name, err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "track-multiple-conflicting-delegations",
|
||||
pending: 3,
|
||||
run: func(name string) {
|
||||
// Send two setcode txs both with C as an authority.
|
||||
if err := pool.addRemoteSync(pricedSetCodeTx(0, 250000, uint256.NewInt(10), uint256.NewInt(3), keyA, []unsignedAuth{{0, keyC}})); err != nil {
|
||||
t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err)
|
||||
}
|
||||
if err := pool.addRemoteSync(pricedSetCodeTx(0, 250000, uint256.NewInt(30), uint256.NewInt(30), keyB, []unsignedAuth{{0, keyC}})); err != nil {
|
||||
t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err)
|
||||
}
|
||||
// Replace the tx from A with a non-setcode tx.
|
||||
if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1000), keyA)); err != nil {
|
||||
t.Fatalf("%s: failed to replace with remote transaction: %v", name, err)
|
||||
}
|
||||
// Make sure we can only pool one tx from keyC since it is still a
|
||||
// pending authority.
|
||||
if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1000), keyC)); err != nil {
|
||||
t.Fatalf("%s: failed to added single pooled for account with pending delegation: %v", name, err)
|
||||
}
|
||||
if err, want := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1000), keyC)), txpool.ErrAccountLimitExceeded; !errors.Is(err, want) {
|
||||
t.Fatalf("%s: error mismatch: want %v, have %v", name, want, err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reject-delegation-from-pending-account",
|
||||
pending: 1,
|
||||
run: func(name string) {
|
||||
// Attempt to submit a delegation from an account with a pending tx.
|
||||
if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1000), keyC)); err != nil {
|
||||
t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err)
|
||||
}
|
||||
if err, want := pool.addRemoteSync(setCodeTx(0, keyA, []unsignedAuth{{1, keyC}})), txpool.ErrAuthorityReserved; !errors.Is(err, want) {
|
||||
t.Fatalf("%s: error mismatch: want %v, have %v", name, want, err)
|
||||
}
|
||||
},
|
||||
},
|
||||
} {
|
||||
tt.run(tt.name)
|
||||
pending, queued := pool.Stats()
|
||||
if pending != tt.pending {
|
||||
t.Fatalf("%s: pending transactions mismatched: have %d, want %d", tt.name, pending, tt.pending)
|
||||
}
|
||||
if queued != tt.queued {
|
||||
t.Fatalf("%s: queued transactions mismatched: have %d, want %d", tt.name, queued, tt.queued)
|
||||
}
|
||||
if err := validatePoolInternals(pool); err != nil {
|
||||
t.Fatalf("%s: pool internal state corrupted: %v", tt.name, err)
|
||||
}
|
||||
pool.Clear()
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmarks the speed of validating the contents of the pending queue of the
|
||||
// transaction pool.
|
||||
func BenchmarkPendingDemotion100(b *testing.B) { benchmarkPendingDemotion(b, 100) }
|
||||
|
|
|
@ -70,17 +70,21 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
|
|||
return fmt.Errorf("%w: transaction size %v, limit %v", ErrOversizedData, tx.Size(), opts.MaxSize)
|
||||
}
|
||||
// Ensure only transactions that have been enabled are accepted
|
||||
if !opts.Config.IsBerlin(head.Number) && tx.Type() != types.LegacyTxType {
|
||||
rules := opts.Config.Rules(head.Number, head.Difficulty.Sign() == 0, head.Time)
|
||||
if !rules.IsBerlin && tx.Type() != types.LegacyTxType {
|
||||
return fmt.Errorf("%w: type %d rejected, pool not yet in Berlin", core.ErrTxTypeNotSupported, tx.Type())
|
||||
}
|
||||
if !opts.Config.IsLondon(head.Number) && tx.Type() == types.DynamicFeeTxType {
|
||||
if !rules.IsLondon && tx.Type() == types.DynamicFeeTxType {
|
||||
return fmt.Errorf("%w: type %d rejected, pool not yet in London", core.ErrTxTypeNotSupported, tx.Type())
|
||||
}
|
||||
if !opts.Config.IsCancun(head.Number, head.Time) && tx.Type() == types.BlobTxType {
|
||||
if !rules.IsCancun && tx.Type() == types.BlobTxType {
|
||||
return fmt.Errorf("%w: type %d rejected, pool not yet in Cancun", core.ErrTxTypeNotSupported, tx.Type())
|
||||
}
|
||||
if !rules.IsPrague && tx.Type() == types.SetCodeTxType {
|
||||
return fmt.Errorf("%w: type %d rejected, pool not yet in Prague", core.ErrTxTypeNotSupported, tx.Type())
|
||||
}
|
||||
// Check whether the init code size has been exceeded
|
||||
if opts.Config.IsShanghai(head.Number, head.Time) && tx.To() == nil && len(tx.Data()) > params.MaxInitCodeSize {
|
||||
if rules.IsShanghai && tx.To() == nil && len(tx.Data()) > params.MaxInitCodeSize {
|
||||
return fmt.Errorf("%w: code size %v, limit %v", core.ErrMaxInitCodeSizeExceeded, len(tx.Data()), params.MaxInitCodeSize)
|
||||
}
|
||||
// Transactions can't be negative. This may never happen using RLP decoded
|
||||
|
@ -109,7 +113,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
|
|||
}
|
||||
// Ensure the transaction has more gas than the bare minimum needed to cover
|
||||
// the transaction metadata
|
||||
intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, true, opts.Config.IsIstanbul(head.Number), opts.Config.IsShanghai(head.Number, head.Time))
|
||||
intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, true, rules.IsIstanbul, rules.IsShanghai)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -154,6 +158,11 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
|
|||
return err
|
||||
}
|
||||
}
|
||||
if tx.Type() == types.SetCodeTxType {
|
||||
if len(tx.SetCodeAuthorizations()) == 0 {
|
||||
return fmt.Errorf("set code tx must have at least one authorization tuple")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -209,6 +218,11 @@ type ValidationOptionsWithState struct {
|
|||
// ExistingCost is a mandatory callback to retrieve an already pooled
|
||||
// transaction's cost with the given nonce to check for overdrafts.
|
||||
ExistingCost func(addr common.Address, nonce uint64) *big.Int
|
||||
|
||||
// KnownConflicts is an optional callback which iterates over the list of
|
||||
// addresses and returns all addresses known to the pool with in-flight
|
||||
// transactions.
|
||||
KnownConflicts func(sender common.Address, authorizers []common.Address) []common.Address
|
||||
}
|
||||
|
||||
// ValidateTransactionWithState is a helper method to check whether a transaction
|
||||
|
@ -262,6 +276,14 @@ func ValidateTransactionWithState(tx *types.Transaction, signer types.Signer, op
|
|||
if used, left := opts.UsedAndLeftSlots(from); left <= 0 {
|
||||
return fmt.Errorf("%w: pooled %d txs", ErrAccountLimitExceeded, used)
|
||||
}
|
||||
|
||||
// Verify no authorizations will invalidate existing transactions known to
|
||||
// the pool.
|
||||
if opts.KnownConflicts != nil {
|
||||
if conflicts := opts.KnownConflicts(from, tx.SetCodeAuthorities()); len(conflicts) > 0 {
|
||||
return fmt.Errorf("%w: authorization conflicts with other known tx", ErrAuthorityReserved)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -483,6 +483,21 @@ func (tx *Transaction) SetCodeAuthorizations() []SetCodeAuthorization {
|
|||
return setcodetx.AuthList
|
||||
}
|
||||
|
||||
// SetCodeAuthorities returns a list of each authorization's corresponding authority.
|
||||
func (tx *Transaction) SetCodeAuthorities() []common.Address {
|
||||
setcodetx, ok := tx.inner.(*SetCodeTx)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
auths := make([]common.Address, 0, len(setcodetx.AuthList))
|
||||
for _, auth := range setcodetx.AuthList {
|
||||
if addr, err := auth.Authority(); err == nil {
|
||||
auths = append(auths, addr)
|
||||
}
|
||||
}
|
||||
return auths
|
||||
}
|
||||
|
||||
// SetTime sets the decoding time of a transaction. This is used by tests to set
|
||||
// arbitrary times and by persistent transaction pools when loading old txs from
|
||||
// disk.
|
||||
|
|
Loading…
Reference in New Issue