Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
lightclient committed Jan 23, 2025
1 parent d3cc618 commit 77301e7
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 6 deletions.
5 changes: 5 additions & 0 deletions core/txpool/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,9 @@ 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")
)
62 changes: 56 additions & 6 deletions core/txpool/legacypool/legacypool.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,12 +217,13 @@ type LegacyPool struct {
locals *accountSet // Set of local transaction to exempt from eviction rules
journal *journal // Journal of local transaction to back up to disk

reserve txpool.AddressReserver // Address reserver to ensure exclusivity across subpools
pending map[common.Address]*list // All currently processable transactions
queue map[common.Address]*list // Queued but non-processable transactions
beats map[common.Address]time.Time // Last heartbeat from each known account
all *lookup // All transactions to allow lookups
priced *pricedList // All transactions sorted by price
reserve txpool.AddressReserver // Address reserver to ensure exclusivity across subpools
pending map[common.Address]*list // All currently processable transactions
queue map[common.Address]*list // Queued but non-processable transactions
beats map[common.Address]time.Time // Last heartbeat from each known account
all *lookup // All transactions to allow lookups
priced *pricedList // All transactions sorted by price
auths map[common.Address]*types.Transaction // All accounts with a pooled authorization

reqResetCh chan *txpoolResetRequest
reqPromoteCh chan *accountSet
Expand Down Expand Up @@ -254,6 +255,7 @@ func New(config Config, chain BlockChain) *LegacyPool {
pending: make(map[common.Address]*list),
queue: make(map[common.Address]*list),
beats: make(map[common.Address]time.Time),
auths: make(map[common.Address]*types.Transaction),
all: newLookup(),
reqResetCh: make(chan *txpoolResetRequest),
reqPromoteCh: make(chan *accountSet),
Expand Down Expand Up @@ -639,6 +641,14 @@ func (pool *LegacyPool) validateTx(tx *types.Transaction) error {
if list := pool.queue[addr]; list != nil {
have += list.Len()
}
// Limit the number of setcode tranasactions per account
if pool.currentState.GetCode(addr) != nil {
if have >= 1 {
return have, 0
} else {
return have, 1 - have
}
}
return have, math.MaxInt
},
ExistingExpenditure: func(addr common.Address) *big.Int {
Expand All @@ -655,6 +665,25 @@ func (pool *LegacyPool) validateTx(tx *types.Transaction) error {
}
return nil
},
KnownConflicts: func(addrs []common.Address) []common.Address {
var conflicts []common.Address
for _, addr := range addrs {
var known bool
if list := pool.pending[addr]; list != nil {
known = true
}
if list := pool.queue[addr]; list != nil {
known = true
}
if _, ok := pool.auths[addr]; ok {
known = true
}
if known {
conflicts = append(conflicts, addr)
}
}
return conflicts
},
}
if err := txpool.ValidateTransactionWithState(tx, pool.signer, opts); err != nil {
return err
Expand Down Expand Up @@ -692,6 +721,7 @@ func (pool *LegacyPool) add(tx *types.Transaction, local bool) (replaced bool, e

// If the address is not yet known, request exclusivity to track the account
// only by this subpool until all transactions are evicted
// TODO: need to track every authority from setcode txs
var (
_, hasPending = pool.pending[from]
_, hasQueued = pool.queue[from]
Expand Down Expand Up @@ -792,6 +822,11 @@ func (pool *LegacyPool) add(tx *types.Transaction, local bool) (replaced bool, e
pool.priced.Put(tx, isLocal)
pool.journalTx(from, tx)
pool.queueTxEvent(tx)
for _, auth := range tx.AuthList() {
if addr, err := auth.Authority(); err == nil {
pool.auths[addr] = tx
}
}
log.Trace("Pooled new executable transaction", "hash", hash, "from", from, "to", tx.To())

// Successful promotion, bump the heartbeat
Expand All @@ -813,6 +848,11 @@ func (pool *LegacyPool) add(tx *types.Transaction, local bool) (replaced bool, e
localGauge.Inc(1)
}
pool.journalTx(from, tx)
for _, auth := range tx.AuthList() {
if addr, err := auth.Authority(); err == nil {
pool.auths[addr] = tx
}
}

log.Trace("Pooled new future transaction", "hash", hash, "from", from, "to", tx.To())
return replaced, nil
Expand Down Expand Up @@ -879,6 +919,10 @@ func (pool *LegacyPool) enqueueTx(hash common.Hash, tx *types.Transaction, local
if _, exist := pool.beats[from]; !exist {
pool.beats[from] = time.Now()
}
for _, auth := range tx.AuthList() {
addr, _ := auth.Authority()
pool.auths[addr] = tx
}
return old != nil, nil
}

Expand Down Expand Up @@ -1129,6 +1173,12 @@ func (pool *LegacyPool) removeTx(hash common.Hash, outofbound bool, unreserve bo
if pool.locals.contains(addr) {
localGauge.Dec(1)
}
// Remove any authorities the pool was tracking.
for _, auth := range tx.AuthList() {
if addr, err := auth.Authority(); err == nil {
pool.auths[addr] = nil
}
}
// Remove the transaction from the pending lists and reset the account nonce
if pending := pool.pending[addr]; pending != nil {
if removed, invalids := pending.Remove(tx); removed {
Expand Down
130 changes: 130 additions & 0 deletions core/txpool/legacypool/legacypool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,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"
Expand Down Expand Up @@ -129,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.AuthorizationList
for _, u := range unsigned {
auth, _ := types.SignAuth(&types.Authorization{
ChainID: params.TestChainConfig.ChainID.Uint64(),
Address: common.Address{0x42},
Nonce: u.nonce,
}, u.key)
authList = append(authList, auth)
}
return types.MustSignNewTx(key, types.LatestSignerForChainID(params.TestChainConfig.ChainID), &types.SetCodeTx{
ChainID: params.TestChainConfig.ChainID.Uint64(),
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{})
Expand Down Expand Up @@ -2525,6 +2559,102 @@ 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
keys := make([]*ecdsa.PrivateKey, 4)
addrs := make([]common.Address, len(keys))
for i := 0; i < len(keys); i++ {
keys[i], _ = crypto.GenerateKey()
addrs[i] = crypto.PubkeyToAddress(keys[i].PublicKey)
testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(params.Ether))
}

aa := common.Address{0xaa, 0xaa}
statedb.SetCode(addrs[0], append(types.DelegationPrefix, aa.Bytes()...))
statedb.SetCode(aa, []byte{byte(vm.ADDRESS), byte(vm.PUSH0), byte(vm.SSTORE)})

// A few situations to test:
// 1. Accounts with delegation set can only have one in-flight transaction.
// 2. Setcode tx should be rejected if any authority has a known pooled tx.
// 3. New txs from senders with pooled delegations should not be accepted.
// 4. Ensure setcode tx can replace itself provided the fee bump is enough.
// 5. Make sure that if a setcode tx is replaced, the auths associated with
// the tx are removed.
// 5.1. This should also work when a self-sponsored setcode tx attempts
// to replace itself.

// make sure auth list recreated correctly after full reorg?
// (in different test?) verify that a setcode tx cannot invalidate a blob tx.

// 1. Send three transactions from a delegated account, verify only one is
// accepted.
if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1), keys[0])); err != nil {
t.Fatalf("failed to add remote transaction: %v", err)
}
if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1), keys[0])); !errors.Is(err, txpool.ErrAccountLimitExceeded) {
t.Fatalf("error mismatch: want %v, have %v", txpool.ErrAccountLimitExceeded, err)
}
if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(10), keys[0])); err != nil {
t.Fatalf("failed to replace with remote transaction: %v", err)
}
// 2. Send two transactions where the first has no conflicting delegations and
// the second should be rejected due to a conflict with the tx sent in 1).
if err := pool.addRemoteSync(setCodeTx(0, keys[1], []unsignedAuth{{1, keys[2]}})); err != nil {
t.Fatalf("failed to add with remote setcode transaction: %v", err)
}
if err := pool.addRemoteSync(setCodeTx(1, keys[1], []unsignedAuth{{1, keys[0]}})); !errors.Is(err, txpool.ErrAuthorityReserved) {
t.Fatalf("error mismatch: want %v, have %v", txpool.ErrAuthorityReserved, err)
}

// 3. Verify key[2] cannot originate another transaction.
if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1), keys[2])); !errors.Is(err, txpool.ErrAuthorityReserved) {
t.Fatalf("error mismatch: want %v, have %v", txpool.ErrAuthorityReserved, err)
}
if err := pool.addRemoteSync(setCodeTx(1, keys[2], []unsignedAuth{{1, keys[2]}})); !errors.Is(err, txpool.ErrAuthorityReserved) {
t.Fatalf("expected to reject tx from in-flight authority: want %v, have %v", txpool.ErrAuthorityReserved, err)
}

// 4. Fee bump the setcode tx send in 2)
if err := pool.addRemoteSync(pricedSetCodeTx(0, 250000, uint256.NewInt(2000), uint256.NewInt(2), keys[1], []unsignedAuth{{1, keys[2]}})); err != nil {
t.Fatalf("failed to add with remote setcode transaction: %v", err)
}

// 5. Fee bump with a different auth list. Make sure that unlocks the authorities.
if err := pool.addRemoteSync(pricedSetCodeTx(0, 250000, uint256.NewInt(3000), uint256.NewInt(3), keys[1], []unsignedAuth{{1, keys[3]}})); err != nil {
t.Fatalf("failed to add with remote setcode transaction: %v", err)
}
if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(10), keys[2])); err != nil {
t.Fatalf("failed to replace with remote transaction: %v", err)
}

// if err := pool.addRemoteSync(setCodeTx(1, keys[3], []unsignedAuth{{1, keys[2]}})); !errors.Is(err, txpool.ErrAuthorityReserved) {
// t.Fatalf("expected to reject tx from in-flight authority: want %v, have %v", txpool.ErrAuthorityReserved, err)
// }

pending, queued := pool.Stats()
if pending != 2 {
t.Fatalf("pending transactions mismatched: have %d, want %d", pending, 1)
}
if queued != 0 {
t.Fatalf("queued transactions mismatched: have %d, want %d", queued, 0)
}
if err := validatePoolInternals(pool); err != nil {
t.Fatalf("pool internal state corrupted: %v", err)
}
}

// Benchmarks the speed of validating the contents of the pending queue of the
// transaction pool.
func BenchmarkPendingDemotion100(b *testing.B) { benchmarkPendingDemotion(b, 100) }
Expand Down
22 changes: 22 additions & 0 deletions core/txpool/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
if !opts.Config.IsCancun(head.Number, head.Time) && tx.Type() == types.BlobTxType {
return fmt.Errorf("%w: type %d rejected, pool not yet in Cancun", core.ErrTxTypeNotSupported, tx.Type())
}
if !opts.Config.IsPrague(head.Number, head.Time) && 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 {
return fmt.Errorf("%w: code size %v, limit %v", core.ErrMaxInitCodeSizeExceeded, len(tx.Data()), params.MaxInitCodeSize)
Expand Down Expand Up @@ -142,6 +145,11 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
return err
}
}
if tx.Type() == types.SetCodeTxType {
if len(tx.AuthList()) == 0 {

Check failure on line 149 in core/txpool/validation.go

View workflow job for this annotation

GitHub Actions / build

tx.AuthList undefined (type *types.Transaction has no field or method AuthList)
return fmt.Errorf("set code tx must have at least one authorization tuple")
}
}
return nil
}

Expand Down Expand Up @@ -197,6 +205,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([]common.Address) []common.Address
}

// ValidateTransactionWithState is a helper method to check whether a transaction
Expand Down Expand Up @@ -250,6 +263,15 @@ 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 {
addrs := append(tx.Authorities(), from)
if conflicts := opts.KnownConflicts(addrs); len(conflicts) > 0 {
return fmt.Errorf("%w: authorization conflicts with other known tx", ErrAuthorityReserved)
}
}
}
return nil
}
15 changes: 15 additions & 0 deletions core/types/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,21 @@ func (tx *Transaction) SetCodeAuthorizations() []SetCodeAuthorization {
return setcodetx.AuthList
}

// Authorities returns a list of each authorization's corresponding authority.
func (tx *Transaction) Authorities() []common.Address {
setcodetx, ok := tx.inner.(*SetCodeTx)
if !ok {
return nil
}
auths := make([]common.Address, 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.
Expand Down

0 comments on commit 77301e7

Please sign in to comment.