core/txpool: allow same delegation in multiple txs

This commit is contained in:
lightclient 2025-01-25 17:48:44 -07:00
parent 487fcd8528
commit e01a04292e
No known key found for this signature in database
GPG key ID: 657913021EF45A6A
2 changed files with 150 additions and 103 deletions

View file

@ -21,6 +21,7 @@ import (
"errors" "errors"
"math" "math"
"math/big" "math/big"
"slices"
"sort" "sort"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -209,13 +210,13 @@ type LegacyPool struct {
currentState *state.StateDB // Current state in the blockchain head currentState *state.StateDB // Current state in the blockchain head
pendingNonces *noncer // Pending state tracking virtual nonces pendingNonces *noncer // Pending state tracking virtual nonces
reserve txpool.AddressReserver // Address reserver to ensure exclusivity across subpools reserve txpool.AddressReserver // Address reserver to ensure exclusivity across subpools
pending map[common.Address]*list // All currently processable transactions pending map[common.Address]*list // All currently processable transactions
queue map[common.Address]*list // Queued but non-processable transactions queue map[common.Address]*list // Queued but non-processable transactions
beats map[common.Address]time.Time // Last heartbeat from each known account beats map[common.Address]time.Time // Last heartbeat from each known account
all *lookup // All transactions to allow lookups all *lookup // All transactions to allow lookups
priced *pricedList // All transactions sorted by price priced *pricedList // All transactions sorted by price
auths map[common.Address]*types.Transaction // All accounts with a pooled authorization auths map[common.Address][]*types.Transaction // All accounts with a pooled authorization
reqResetCh chan *txpoolResetRequest reqResetCh chan *txpoolResetRequest
reqPromoteCh chan *accountSet reqPromoteCh chan *accountSet
@ -247,7 +248,7 @@ func New(config Config, chain BlockChain) *LegacyPool {
pending: make(map[common.Address]*list), pending: make(map[common.Address]*list),
queue: make(map[common.Address]*list), queue: make(map[common.Address]*list),
beats: make(map[common.Address]time.Time), beats: make(map[common.Address]time.Time),
auths: make(map[common.Address]*types.Transaction), auths: make(map[common.Address][]*types.Transaction),
all: newLookup(), all: newLookup(),
reqResetCh: make(chan *txpoolResetRequest), reqResetCh: make(chan *txpoolResetRequest),
reqPromoteCh: make(chan *accountSet), reqPromoteCh: make(chan *accountSet),
@ -568,13 +569,9 @@ func (pool *LegacyPool) validateTx(tx *types.Transaction) error {
if list := pool.queue[addr]; list != nil { if list := pool.queue[addr]; list != nil {
have += list.Len() have += list.Len()
} }
// Limit the number of setcode tranasactions per account
if pool.currentState.GetCode(addr) != nil { if pool.currentState.GetCode(addr) != nil {
if have >= 1 { // Allow at most one in-flight tx for delegated accounts.
return have, 0 return have, max(0, 1-have)
} else {
return have, 1 - have
}
} }
return have, math.MaxInt return have, math.MaxInt
}, },
@ -592,12 +589,14 @@ func (pool *LegacyPool) validateTx(tx *types.Transaction) error {
} }
return nil return nil
}, },
KnownConflicts: func(sender common.Address, addrs []common.Address) []common.Address { KnownConflicts: func(from common.Address, auths []common.Address) []common.Address {
var conflicts []common.Address var conflicts []common.Address
if _, ok := pool.auths[sender]; ok { // The transaction sender cannot have an in-flight authorization.
conflicts = append(conflicts, sender) if _, ok := pool.auths[from]; ok {
conflicts = append(conflicts, from)
} }
for _, addr := range addrs { // Authorities cannot conflict with any pending or queued transactions.
for _, addr := range auths {
var known bool var known bool
if list := pool.pending[addr]; list != nil { if list := pool.pending[addr]; list != nil {
known = true known = true
@ -605,9 +604,6 @@ func (pool *LegacyPool) validateTx(tx *types.Transaction) error {
if list := pool.queue[addr]; list != nil { if list := pool.queue[addr]; list != nil {
known = true known = true
} }
if _, ok := pool.auths[addr]; ok {
known = true
}
if known { if known {
conflicts = append(conflicts, addr) conflicts = append(conflicts, addr)
} }
@ -644,7 +640,6 @@ func (pool *LegacyPool) add(tx *types.Transaction) (replaced bool, err error) {
// If the address is not yet known, request exclusivity to track the account // If the address is not yet known, request exclusivity to track the account
// only by this subpool until all transactions are evicted // only by this subpool until all transactions are evicted
// TODO: need to track every authority from setcode txs
var ( var (
_, hasPending = pool.pending[from] _, hasPending = pool.pending[from]
_, hasQueued = pool.queue[from] _, hasQueued = pool.queue[from]
@ -744,9 +739,7 @@ func (pool *LegacyPool) add(tx *types.Transaction) (replaced bool, err error) {
pool.all.Add(tx) pool.all.Add(tx)
pool.priced.Put(tx) pool.priced.Put(tx)
pool.queueTxEvent(tx) pool.queueTxEvent(tx)
for _, addr := range tx.Authorities() { pool.addAuthorities(tx)
pool.auths[addr] = tx
}
log.Trace("Pooled new executable transaction", "hash", hash, "from", from, "to", tx.To()) log.Trace("Pooled new executable transaction", "hash", hash, "from", from, "to", tx.To())
// Successful promotion, bump the heartbeat // Successful promotion, bump the heartbeat
@ -758,9 +751,7 @@ func (pool *LegacyPool) add(tx *types.Transaction) (replaced bool, err error) {
if err != nil { if err != nil {
return false, err return false, err
} }
for _, addr := range tx.Authorities() { pool.addAuthorities(tx)
pool.auths[addr] = tx
}
log.Trace("Pooled new future transaction", "hash", hash, "from", from, "to", tx.To()) log.Trace("Pooled new future transaction", "hash", hash, "from", from, "to", tx.To())
return replaced, nil return replaced, nil
@ -828,10 +819,7 @@ func (pool *LegacyPool) enqueueTx(hash common.Hash, tx *types.Transaction, addAl
if _, exist := pool.beats[from]; !exist { if _, exist := pool.beats[from]; !exist {
pool.beats[from] = time.Now() pool.beats[from] = time.Now()
} }
for _, auth := range tx.SetCodeAuthorizations() { pool.addAuthorities(tx)
addr, _ := auth.Authority()
pool.auths[addr] = tx
}
return old != nil, nil return old != nil, nil
} }
@ -1086,9 +1074,40 @@ func (pool *LegacyPool) removeTx(hash common.Hash, outofbound bool, unreserve bo
return 0 return 0
} }
// addAuthorities tracks the supplied tx in relation to each authority it
// specifies.
func (pool *LegacyPool) addAuthorities(tx *types.Transaction) {
for _, addr := range tx.Authorities() {
list, ok := pool.auths[addr]
if !ok {
list = []*types.Transaction{}
}
if slices.Contains(list, tx) {
// Don't add duplicates.
continue
}
list = append(list, tx)
pool.auths[addr] = list
}
}
// removeAuthorities stops tracking the supplied tx in relation to its
// authorities.
func (pool *LegacyPool) removeAuthorities(tx *types.Transaction) { func (pool *LegacyPool) removeAuthorities(tx *types.Transaction) {
for _, addr := range tx.Authorities() { for _, addr := range tx.Authorities() {
delete(pool.auths, addr) // Remove tx from tracker.
list := pool.auths[addr]
if i := slices.Index(list, tx); i >= 0 {
list = append(list[:i], list[i+1:]...)
} else {
log.Error("Authority with untracked tx", "addr", addr, "hash", tx.Hash())
}
if len(list) == 0 {
// If list is newly empty, delete it entirely.
delete(pool.auths, addr)
continue
}
pool.auths[addr] = list
} }
} }
@ -1823,5 +1842,5 @@ func (pool *LegacyPool) Clear() {
pool.pending = make(map[common.Address]*list) pool.pending = make(map[common.Address]*list)
pool.queue = make(map[common.Address]*list) pool.queue = make(map[common.Address]*list)
pool.pendingNonces = newNoncer(pool.currentState) pool.pendingNonces = newNoncer(pool.currentState)
pool.auths = make(map[common.Address]*types.Transaction) pool.auths = make(map[common.Address][]*types.Transaction)
} }

View file

@ -2212,13 +2212,17 @@ func TestSetCodeTransactions(t *testing.T) {
defer pool.Close() defer pool.Close()
// Create the test accounts // Create the test accounts
keys := make([]*ecdsa.PrivateKey, 4) var (
addrs := make([]common.Address, len(keys)) keyA, _ = crypto.GenerateKey()
for i := 0; i < len(keys); i++ { keyB, _ = crypto.GenerateKey()
keys[i], _ = crypto.GenerateKey() keyC, _ = crypto.GenerateKey()
addrs[i] = crypto.PubkeyToAddress(keys[i].PublicKey) addrA = crypto.PubkeyToAddress(keyA.PublicKey)
testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(params.Ether)) 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))
// A few situations to test: // A few situations to test:
// 1. Accounts with delegation set can only have one in-flight transaction. // 1. Accounts with delegation set can only have one in-flight transaction.
@ -2237,106 +2241,140 @@ func TestSetCodeTransactions(t *testing.T) {
name string name string
pending int pending int
queued int queued int
run func() error 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", name: "only-one-in-flight",
pending: 1, pending: 1,
run: func() error { run: func(name 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.
aa := common.Address{0xaa, 0xaa} aa := common.Address{0xaa, 0xaa}
statedb.SetCode(addrs[0], append(types.DelegationPrefix, aa.Bytes()...)) statedb.SetCode(addrA, append(types.DelegationPrefix, aa.Bytes()...))
statedb.SetCode(aa, []byte{byte(vm.ADDRESS), byte(vm.PUSH0), byte(vm.SSTORE)}) statedb.SetCode(aa, []byte{byte(vm.ADDRESS), byte(vm.PUSH0), byte(vm.SSTORE)})
// Send transactions. First is accepted, second is rejected. // Send transactions. First is accepted, second is rejected.
if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1), keys[0])); err != nil { if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1), keyA)); err != nil {
return fmt.Errorf("failed to add remote transaction: %v", err) t.Fatalf("%s: failed to add remote transaction: %v", name, err)
} }
if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1), keys[0])); !errors.Is(err, txpool.ErrAccountLimitExceeded) { if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1), keyA)); !errors.Is(err, txpool.ErrAccountLimitExceeded) {
return fmt.Errorf("error mismatch: want %v, have %v", txpool.ErrAccountLimitExceeded, err) t.Fatalf("%s: error mismatch: want %v, have %v", name, txpool.ErrAccountLimitExceeded, err)
} }
// Also check gapped transaction. // Also check gapped transaction.
if err := pool.addRemoteSync(pricedTransaction(2, 100000, big.NewInt(1), keys[0])); !errors.Is(err, txpool.ErrAccountLimitExceeded) { if err := pool.addRemoteSync(pricedTransaction(2, 100000, big.NewInt(1), keyA)); !errors.Is(err, txpool.ErrAccountLimitExceeded) {
return fmt.Errorf("error mismatch: want %v, have %v", txpool.ErrAccountLimitExceeded, err) t.Fatalf("%s: error mismatch: want %v, have %v", name, txpool.ErrAccountLimitExceeded, err)
} }
// Replace by fee. // Replace by fee.
if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(10), keys[0])); err != nil { if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(10), keyA)); err != nil {
return fmt.Errorf("failed to replace with remote transaction: %v", err) t.Fatalf("%s: failed to replace with remote transaction: %v", name, err)
} }
return nil
}, },
}, },
{ {
name: "reject-setcode-tx-with-pending-authority-tx", name: "allow-setcode-tx-with-pending-authority-tx",
pending: 1, pending: 2,
run: func() error { run: func(name string) {
// Send two transactions where the first has no conflicting delegations and // 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). // the second should be allowed despite conflicting with the authorities in 1).
if err := pool.addRemoteSync(setCodeTx(0, keys[1], []unsignedAuth{{1, keys[2]}})); err != nil { if err := pool.addRemoteSync(setCodeTx(0, keyA, []unsignedAuth{{1, keyC}})); err != nil {
return fmt.Errorf("failed to add with remote setcode transaction: %v", err) t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err)
} }
if err := pool.addRemoteSync(setCodeTx(1, keys[1], []unsignedAuth{{1, keys[2]}})); !errors.Is(err, txpool.ErrAuthorityReserved) { if err := pool.addRemoteSync(setCodeTx(0, keyB, []unsignedAuth{{1, keyC}})); err != nil {
return fmt.Errorf("error mismatch: want %v, have %v", txpool.ErrAuthorityReserved, err) t.Fatalf("%s: error mismatch: want %v, have %v", txpool.ErrAuthorityReserved, name, err)
} }
return nil
}, },
}, },
{ {
name: "reject-tx-from-pooled-delegation", name: "reject-tx-from-pooled-delegation",
pending: 1, pending: 1,
run: func() error { run: func(name string) {
// Verify key[2] cannot originate another transaction when it has a pooled delegation. // Verify C cannot originate another transaction when it has a pooled delegation.
if err := pool.addRemoteSync(setCodeTx(0, keys[0], []unsignedAuth{{0, keys[2]}})); err != nil { if err := pool.addRemoteSync(setCodeTx(0, keyA, []unsignedAuth{{0, keyC}})); err != nil {
return fmt.Errorf("failed to add with remote setcode transaction: %v", err) t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err)
} }
if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1), keys[2])); !errors.Is(err, txpool.ErrAuthorityReserved) { if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1), keyC)); !errors.Is(err, txpool.ErrAuthorityReserved) {
return fmt.Errorf("error mismatch: want %v, have %v", txpool.ErrAuthorityReserved, err) t.Fatalf("%s: error mismatch: want %v, have %v", name, txpool.ErrAuthorityReserved, err)
} }
// Also check gapped transaction is rejected. // Also check gapped transaction is rejected.
if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1), keys[2])); !errors.Is(err, txpool.ErrAuthorityReserved) { if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1), keyC)); !errors.Is(err, txpool.ErrAuthorityReserved) {
return fmt.Errorf("error mismatch: want %v, have %v", txpool.ErrAuthorityReserved, err) t.Fatalf("%s: error mismatch: want %v, have %v", name, txpool.ErrAuthorityReserved, err)
} }
return nil
}, },
}, },
{ {
name: "replace-by-fee-setcode-tx", name: "replace-by-fee-setcode-tx",
pending: 1, pending: 1,
run: func() error { run: func(name string) {
// 4. Fee bump the setcode tx send. // 4. Fee bump the setcode tx send.
if err := pool.addRemoteSync(setCodeTx(0, keys[1], []unsignedAuth{{1, keys[2]}})); err != nil { if err := pool.addRemoteSync(setCodeTx(0, keyB, []unsignedAuth{{1, keyC}})); err != nil {
return fmt.Errorf("failed to add with remote setcode transaction: %v", err) 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), keys[1], []unsignedAuth{{0, keys[2]}})); err != nil { if err := pool.addRemoteSync(pricedSetCodeTx(0, 250000, uint256.NewInt(2000), uint256.NewInt(2), keyB, []unsignedAuth{{0, keyC}})); err != nil {
t.Fatalf("failed to add with remote setcode transaction: %v", err) t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err)
} }
return nil
}, },
}, },
{ {
name: "allow-tx-from-replaced-authority", name: "allow-tx-from-replaced-authority",
pending: 2, pending: 2,
run: func() error { run: func(name string) {
// Fee bump with a different auth list. Make sure that unlocks the authorities. // 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), keys[0], []unsignedAuth{{0, keys[1]}})); err != nil { if err := pool.addRemoteSync(pricedSetCodeTx(0, 250000, uint256.NewInt(10), uint256.NewInt(3), keyA, []unsignedAuth{{0, keyB}})); err != nil {
t.Fatalf("failed to add with remote setcode transaction: %v", err) 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), keys[0], []unsignedAuth{{0, keys[2]}})); err != nil { if err := pool.addRemoteSync(pricedSetCodeTx(0, 250000, uint256.NewInt(3000), uint256.NewInt(300), keyA, []unsignedAuth{{0, keyC}})); err != nil {
t.Fatalf("failed to add with remote setcode transaction: %v", err) t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err)
} }
fmt.Println(pool.auths) // Now send a regular tx from B.
// Now send a regular tx from keys[1]. if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(10), keyB)); err != nil {
if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(10), keys[1])); err != nil { t.Fatalf("%s: failed to replace with remote transaction: %v", name, err)
t.Fatalf("failed to replace with remote transaction: %v", 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: 2,
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 cannot send from keyC since it is still a pending authority.
if err, want := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1000), keyC)), txpool.ErrAuthorityReserved; !errors.Is(err, want) {
t.Fatalf("%s: error mismatch: want %v, have %v", name, want, err)
} }
return nil
}, },
}, },
} { } {
if err := tt.run(); err != nil { tt.run(tt.name)
t.Fatalf("%s: %v", tt.name, err)
}
pending, queued := pool.Stats() pending, queued := pool.Stats()
if pending != tt.pending { if pending != tt.pending {
t.Fatalf("%s: pending transactions mismatched: have %d, want %d", tt.name, pending, tt.pending) t.Fatalf("%s: pending transactions mismatched: have %d, want %d", tt.name, pending, tt.pending)
@ -2349,16 +2387,6 @@ func TestSetCodeTransactions(t *testing.T) {
} }
pool.Clear() pool.Clear()
} }
/*
pool.Clear()
// 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)
// }
*/
} }
// Benchmarks the speed of validating the contents of the pending queue of the // Benchmarks the speed of validating the contents of the pending queue of the