From 217b069ec094ef5457d80cc93d350c2f5baa5592 Mon Sep 17 00:00:00 2001 From: Daniel Liu <139250065@qq.com> Date: Mon, 19 Jan 2026 16:49:34 +0800 Subject: [PATCH] core/txpool, types: add support for SetCode transactions #31073 (#1922) --- core/txpool/errors.go | 5 + core/txpool/legacypool/legacypool.go | 122 ++++++++++- core/txpool/legacypool/legacypool_test.go | 243 +++++++++++++++++++++- core/txpool/validation.go | 28 ++- core/types/transaction.go | 15 ++ core/types/tx_setcode.go | 2 +- 6 files changed, 401 insertions(+), 14 deletions(-) diff --git a/core/txpool/errors.go b/core/txpool/errors.go index 8911e59676..7bfb5283be 100644 --- a/core/txpool/errors.go +++ b/core/txpool/errors.go @@ -65,6 +65,11 @@ var ( // 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") + ErrZeroGasPrice = errors.New("zero gas price") ErrUnderMinGasPrice = errors.New("under min gas price") diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index df127e9049..fd77c161ed 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -22,6 +22,7 @@ import ( "fmt" "math" "math/big" + "slices" "sort" "sync" "sync/atomic" @@ -214,6 +215,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 @@ -297,7 +312,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 @@ -622,7 +637,8 @@ func (pool *LegacyPool) validateTxBasics(tx *types.Transaction, local bool) erro Accept: 0 | 1<= 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) } + +// Clear implements txpool.SubPool, removing all tracked txs from the pool +// and rotating the journal. +func (pool *LegacyPool) Clear() { + pool.mu.Lock() + defer pool.mu.Unlock() + + // unreserve each tracked account. Ideally, we could just clear the + // reservation map in the parent txpool context. However, if we clear in + // parent context, to avoid exposing the subpool lock, we have to lock the + // reservations and then lock each subpool. + // + // This creates the potential for a deadlock situation: + // + // * TxPool.Clear locks the reservations + // * a new transaction is received which locks the subpool mutex + // * TxPool.Clear attempts to lock subpool mutex + // + // The transaction addition may attempt to reserve the sender addr which + // can't happen until Clear releases the reservation lock. Clear cannot + // acquire the subpool lock until the transaction addition is completed. + for _, tx := range pool.all.locals { + senderAddr, _ := types.Sender(pool.signer, tx) + pool.reserve(senderAddr, false) + } + for _, tx := range pool.all.remotes { + senderAddr, _ := types.Sender(pool.signer, tx) + pool.reserve(senderAddr, false) + } + pool.all = newLookup() + pool.priced = newPricedList(pool.all) + pool.pending = make(map[common.Address]*list) + pool.queue = make(map[common.Address]*list) + pool.pendingNonces = newNoncer(pool.currentState) +} diff --git a/core/txpool/legacypool/legacypool_test.go b/core/txpool/legacypool/legacypool_test.go index 1237447004..c5e513372b 100644 --- a/core/txpool/legacypool/legacypool_test.go +++ b/core/txpool/legacypool/legacypool_test.go @@ -38,10 +38,12 @@ import ( "github.com/XinFinOrg/XDPoSChain/core/tracing" "github.com/XinFinOrg/XDPoSChain/core/txpool" "github.com/XinFinOrg/XDPoSChain/core/types" + "github.com/XinFinOrg/XDPoSChain/core/vm" "github.com/XinFinOrg/XDPoSChain/crypto" "github.com/XinFinOrg/XDPoSChain/event" "github.com/XinFinOrg/XDPoSChain/params" "github.com/XinFinOrg/XDPoSChain/trie" + "github.com/holiman/uint256" ) var ( @@ -94,9 +96,10 @@ func (bc *testBlockChain) Config() *params.ChainConfig { func (bc *testBlockChain) CurrentBlock() *types.Header { return &types.Header{ - Root: types.EmptyRootHash, - Number: new(big.Int), - GasLimit: bc.gasLimit.Load(), + Root: types.EmptyRootHash, + Number: new(big.Int), + Difficulty: common.Big0, + GasLimit: bc.gasLimit.Load(), } } @@ -144,6 +147,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.MustFromBig(common.MinGasPrice), 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{}) @@ -2588,6 +2624,207 @@ 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(new(big.Int).SetUint64(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)) + + minGasPrice := new(big.Int).Set(common.MinGasPrice) + minGasFee := uint256.MustFromBig(minGasPrice) + doubleGasFee := new(uint256.Int).Mul(new(uint256.Int).Set(minGasFee), uint256.NewInt(2)) + tripleGasFee := new(uint256.Int).Mul(new(uint256.Int).Set(minGasFee), uint256.NewInt(3)) + legacyReplacePrice := new(big.Int).Mul(new(big.Int).Set(minGasPrice), big.NewInt(10)) + + 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, minGasPrice, keyA)); err != nil { + t.Fatalf("%s: failed to add remote transaction: %v", name, err) + } + if err := pool.addRemoteSync(pricedTransaction(1, 100000, new(big.Int).Set(minGasPrice), 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, new(big.Int).Set(minGasPrice), 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, legacyReplacePrice, 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, new(big.Int).Set(minGasPrice), 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, new(big.Int).Set(minGasPrice), 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, doubleGasFee, 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, minGasFee, 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, tripleGasFee, 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, new(big.Int).Set(minGasPrice), 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, minGasFee, 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, tripleGasFee, 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, new(big.Int).Set(legacyReplacePrice), 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, new(big.Int).Set(minGasPrice), 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, minGasFee, 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, tripleGasFee, 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, new(big.Int).Set(legacyReplacePrice), 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, new(big.Int).Set(minGasPrice), 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, new(big.Int).Set(minGasPrice), 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, new(big.Int).Set(minGasPrice), 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) } diff --git a/core/txpool/validation.go b/core/txpool/validation.go index d16b489388..69d0e85a69 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -57,11 +57,15 @@ 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.IsEIP1559(head.Number) && tx.Type() != types.LegacyTxType { + rules := opts.Config.Rules(head.Number) + if !rules.IsEIP1559 && tx.Type() != types.LegacyTxType { return fmt.Errorf("%w: type %d rejected, pool not yet in EIP1559", 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.IsEIP1559(head.Number) && tx.To() == nil && len(tx.Data()) > params.MaxInitCodeSize { + if rules.IsEIP1559 && 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 @@ -111,13 +115,18 @@ 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.IsEIP1559(head.Number)) + intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, true, rules.IsEIP1559) if err != nil { return err } if tx.Gas() < intrGas { return fmt.Errorf("%w: needed %v, allowed %v", core.ErrIntrinsicGas, intrGas, tx.Gas()) } + 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 } @@ -145,6 +154,11 @@ type ValidationOptionsWithState struct { // 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 + Trc21FeeCapacity map[common.Address]*big.Int PendingNonce func(addr common.Address) uint64 @@ -221,6 +235,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) + } + } } // Ensure sender and receiver are not in black list diff --git a/core/types/transaction.go b/core/types/transaction.go index d5164aaa66..d3f6959835 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -400,6 +400,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. diff --git a/core/types/tx_setcode.go b/core/types/tx_setcode.go index dc64ace905..8726b1f788 100644 --- a/core/types/tx_setcode.go +++ b/core/types/tx_setcode.go @@ -88,7 +88,7 @@ type authorizationMarshaling struct { } // SignSetCode creates a signed SetCode authorization. -func SignSetCode(auth SetCodeAuthorization, prv *ecdsa.PrivateKey) (SetCodeAuthorization, error) { +func SignSetCode(prv *ecdsa.PrivateKey, auth SetCodeAuthorization) (SetCodeAuthorization, error) { sighash := auth.sigHash() sig, err := crypto.Sign(sighash[:], prv) if err != nil {