diff --git a/core/blockchain_test.go b/core/blockchain_test.go index cd24da39cd..8c5e912c58 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -19,7 +19,6 @@ package core import ( "errors" "fmt" - "github.com/ethereum/go-ethereum/eth/tracers/logger" "io/ioutil" "math/big" "math/rand" @@ -28,6 +27,9 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/eth/tracers/logger" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/ethash" @@ -3337,3 +3339,268 @@ func TestEIP1559Transition(t *testing.T) { t.Fatalf("sender balance incorrect: expected %d, got %d", expected, actual) } } + +func TestSponsoredTxTransitionBeforeMiko(t *testing.T) { + var chainConfig params.ChainConfig + + chainConfig.HomesteadBlock = common.Big0 + chainConfig.EIP150Block = common.Big0 + chainConfig.EIP155Block = common.Big0 + chainConfig.ChainID = big.NewInt(2020) + + engine := ethash.NewFaker() + db := rawdb.NewMemoryDatabase() + + recipient := common.HexToAddress("1000000000000000000000000000000000000001") + senderKey, err := crypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + payerKey, err := crypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + + gspec := &Genesis{ + Config: &chainConfig, + } + genesis := gspec.MustCommit(db) + chain, err := NewBlockChain(db, nil, &chainConfig, engine, vm.Config{}, nil, nil) + if err != nil { + t.Fatalf("Failed to create blockchain, err %s", err) + } + defer chain.Stop() + mikoSigner := types.NewMikoSigner(big.NewInt(2020)) + + innerTx := types.SponsoredTx{ + ChainID: big.NewInt(2020), + Nonce: 1, + GasTipCap: big.NewInt(100000), + GasFeeCap: big.NewInt(100000), + Gas: 1000, + To: &recipient, + Value: big.NewInt(10), + ExpiredTime: 1000, + } + + innerTx.PayerR, innerTx.PayerS, innerTx.PayerV, err = types.PayerSign( + payerKey, + mikoSigner, + crypto.PubkeyToAddress(senderKey.PublicKey), + &innerTx, + ) + if err != nil { + t.Fatalf("Payer fails to sign transaction, err %s", err) + } + + tx, err := types.SignNewTx(senderKey, mikoSigner, &innerTx) + if err != nil { + t.Fatalf("Fail to sign transaction, err %s", err) + } + + block := GenerateBadBlock(genesis, engine, types.Transactions{tx}, &chainConfig) + _, err = chain.InsertChain(types.Blocks{block}) + want := fmt.Errorf("could not apply tx %d [%v]: %w", 0, tx.Hash().String(), ErrTxTypeNotSupported) + if err == nil || err.Error() != want.Error() { + t.Fatalf("Expect error %s, get %s", want, err) + } +} + +func TestSponsoredTxTransition(t *testing.T) { + var chainConfig params.ChainConfig + + chainConfig.HomesteadBlock = common.Big0 + chainConfig.EIP150Block = common.Big0 + chainConfig.EIP155Block = common.Big0 + chainConfig.MikoBlock = common.Big0 + chainConfig.ChainID = big.NewInt(2020) + + engine := ethash.NewFaker() + db := rawdb.NewMemoryDatabase() + + recipient := common.HexToAddress("1000000000000000000000000000000000000001") + senderKey, err := crypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + senderAddr := crypto.PubkeyToAddress(senderKey.PublicKey) + payerKey, err := crypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + payerAddr := crypto.PubkeyToAddress(payerKey.PublicKey) + adminKey, err := crypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + adminAddr := crypto.PubkeyToAddress(adminKey.PublicKey) + + gspec := &Genesis{ + Config: &chainConfig, + Timestamp: 2000, + Alloc: GenesisAlloc{ + adminAddr: {Balance: math.BigPow(10, 18)}, + }, + } + genesis := gspec.MustCommit(db) + chain, err := NewBlockChain(db, nil, &chainConfig, engine, vm.Config{}, nil, nil) + if err != nil { + t.Fatalf("Failed to create blockchain, err %s", err) + } + defer chain.Stop() + mikoSigner := types.NewMikoSigner(big.NewInt(2020)) + + // 1. Same payer and sender in sponsored tx + innerTx := types.SponsoredTx{ + ChainID: big.NewInt(2020), + Nonce: 0, + GasTipCap: big.NewInt(100000), + GasFeeCap: big.NewInt(100000), + Gas: 30000, + To: &recipient, + Value: big.NewInt(10), + ExpiredTime: 1000, + } + + innerTx.PayerR, innerTx.PayerS, innerTx.PayerV, err = types.PayerSign( + payerKey, + mikoSigner, + crypto.PubkeyToAddress(payerKey.PublicKey), + &innerTx, + ) + if err != nil { + t.Fatalf("Payer fails to sign transaction, err %s", err) + } + + sponsoredTx, err := types.SignNewTx(payerKey, mikoSigner, &innerTx) + if err != nil { + t.Fatalf("Fail to sign transaction, err %s", err) + } + + block := GenerateBadBlock(genesis, engine, types.Transactions{sponsoredTx}, &chainConfig) + _, err = chain.InsertChain(types.Blocks{block}) + if err == nil || !errors.Is(err, types.ErrSamePayerSenderSponsoredTx) { + t.Fatalf("Expect error %s, get %s", types.ErrSamePayerSenderSponsoredTx, err) + } + + // 2. Expired sponsored tx + + innerTx.PayerR, innerTx.PayerS, innerTx.PayerV, err = types.PayerSign( + payerKey, + mikoSigner, + crypto.PubkeyToAddress(senderKey.PublicKey), + &innerTx, + ) + if err != nil { + t.Fatalf("Payer fails to sign transaction, err %s", err) + } + + sponsoredTx, err = types.SignNewTx(senderKey, mikoSigner, &innerTx) + if err != nil { + t.Fatalf("Fail to sign transaction, err %s", err) + } + + block = GenerateBadBlock(genesis, engine, types.Transactions{sponsoredTx}, &chainConfig) + _, err = chain.InsertChain(types.Blocks{block}) + if err == nil || !errors.Is(err, ErrExpiredSponsoredTx) { + t.Fatalf("Expect error %s, get %s", ErrExpiredSponsoredTx, err) + } + + // 3. Gas tip cap and gas fee cap are different + innerTx.ExpiredTime = 3000 + innerTx.GasTipCap = new(big.Int).Add(innerTx.GasFeeCap, common.Big1) + innerTx.PayerR, innerTx.PayerS, innerTx.PayerV, err = types.PayerSign( + payerKey, + mikoSigner, + crypto.PubkeyToAddress(senderKey.PublicKey), + &innerTx, + ) + if err != nil { + t.Fatalf("Payer fails to sign transaction, err %s", err) + } + + sponsoredTx, err = types.SignNewTx(senderKey, mikoSigner, &innerTx) + if err != nil { + t.Fatalf("Fail to sign transaction, err %s", err) + } + + block = GenerateBadBlock(genesis, engine, types.Transactions{sponsoredTx}, &chainConfig) + _, err = chain.InsertChain(types.Blocks{block}) + if err == nil || !errors.Is(err, ErrDifferentFeeCapTipCap) { + t.Fatalf("Expect error %s, get %s", ErrDifferentFeeCapTipCap, err) + } + + // 4. Payer does not have sufficient fund + innerTx.GasTipCap = innerTx.GasFeeCap + innerTx.PayerR, innerTx.PayerS, innerTx.PayerV, err = types.PayerSign( + payerKey, + mikoSigner, + crypto.PubkeyToAddress(senderKey.PublicKey), + &innerTx, + ) + if err != nil { + t.Fatalf("Payer fails to sign transaction, err %s", err) + } + + sponsoredTx, err = types.SignNewTx(senderKey, mikoSigner, &innerTx) + if err != nil { + t.Fatalf("Fail to sign transaction, err %s", err) + } + + block = GenerateBadBlock(genesis, engine, types.Transactions{sponsoredTx}, &chainConfig) + _, err = chain.InsertChain(types.Blocks{block}) + if err == nil || !errors.Is(err, ErrInsufficientPayerFunds) { + t.Fatalf("Expect error %s, get %s", ErrInsufficientPayerFunds, err) + } + + // 5. Sender does not have sufficient fund + gasFee := new(big.Int).Mul(innerTx.GasFeeCap, new(big.Int).SetUint64(innerTx.Gas)) + blocks, _ := GenerateChain(&chainConfig, genesis, engine, db, 1, func(i int, bg *BlockGen) { + tx, err := types.SignTx(types.NewTransaction(0, payerAddr, gasFee, params.TxGas, bg.header.BaseFee, nil), mikoSigner, adminKey) + if err != nil { + t.Fatal(err) + } + + bg.AddTx(tx) + }, true) + _, err = chain.InsertChain(blocks) + if err != nil { + t.Fatalf("Failed to insert blocks, err %s", err) + } + + block = GenerateBadBlock(blocks[0], engine, types.Transactions{sponsoredTx}, &chainConfig) + _, err = chain.InsertChain(types.Blocks{block}) + if err == nil || !errors.Is(err, ErrInsufficientSenderFunds) { + t.Fatalf("Expect error %s, get %s", ErrInsufficientSenderFunds, err) + } + + // 5. Successfully add tx + blocks, _ = GenerateChain(&chainConfig, blocks[0], engine, db, 1, func(i int, bg *BlockGen) { + tx, err := types.SignTx(types.NewTransaction(1, senderAddr, innerTx.Value, params.TxGas, bg.header.BaseFee, nil), mikoSigner, adminKey) + if err != nil { + t.Fatal(err) + } + + bg.AddTx(tx) + bg.AddTx(sponsoredTx) + }, true) + _, err = chain.InsertChain(blocks) + if err != nil { + t.Fatalf("Failed to insert blocks, err %s", err) + } + + statedb, _ := chain.State() + // Check sender's balance after sponsored tx + have := statedb.GetBalance(senderAddr) + want := common.Big0 + if have.Cmp(want) != 0 { + t.Fatalf("Expect sender's balance %d, get %d", want.Uint64(), have.Uint64()) + } + // Check payer's balance after sponsored tx + // Transfer tx costs 21000 gas so 9000 gas is refunded + want = new(big.Int).Mul(innerTx.GasFeeCap, big.NewInt(9000)) + have = statedb.GetBalance(payerAddr) + if have.Cmp(want) != 0 { + t.Fatalf("Expect sender's balance %d, get %d", want.Uint64(), have.Uint64()) + } +} diff --git a/core/error.go b/core/error.go index 51ebefc137..2c68fb0a41 100644 --- a/core/error.go +++ b/core/error.go @@ -96,4 +96,19 @@ var ( // ErrSenderNoEOA is returned if the sender of a transaction is a contract. ErrSenderNoEOA = errors.New("sender not an eoa") + + // ErrExpiredSponsoredTx is returned if the sponsored transaction is expired. + ErrExpiredSponsoredTx = errors.New("sponsored transaction is expired") + + // ErrInsufficientPayerFunds is returned if the gas fee cost of executing a transaction + // is higher than the balance of the payer's account. + ErrInsufficientPayerFunds = errors.New("insufficient payer funds for gas * price") + + // ErrInsufficientSenderFunds is returned if the value in transaction + // is higher than the balance of the user's account. + ErrInsufficientSenderFunds = errors.New("insufficient sender funds for value") + + // ErrDifferentFeeCapTipCap is returned if fee cap and tip cap are different + // when dynamic gas fee is not supported + ErrDifferentFeeCapTipCap = errors.New("gas fee cap and gas tip cap are different") ) diff --git a/core/state_processor.go b/core/state_processor.go index 8f3ef145a1..3aa8edbfc3 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -144,9 +144,12 @@ func applyTransaction( from := msg.From() // Check if sender and recipient are blacklisted + payer := msg.Payer() if config.Consortium != nil && config.IsOdysseus(blockNumber) { contractAddr := config.BlacklistContractAddress - if state.IsAddressBlacklisted(statedb, contractAddr, &from) || state.IsAddressBlacklisted(statedb, contractAddr, msg.To()) { + if state.IsAddressBlacklisted(statedb, contractAddr, &from) || + state.IsAddressBlacklisted(statedb, contractAddr, msg.To()) || + state.IsAddressBlacklisted(statedb, contractAddr, &payer) { return nil, nil, ErrAddressBlacklisted } } diff --git a/core/state_transition.go b/core/state_transition.go index cc73d2f2d6..90b6484104 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -18,10 +18,11 @@ package core import ( "fmt" - "github.com/ethereum/go-ethereum/consensus" "math" "math/big" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/common" cmath "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core/types" @@ -78,6 +79,12 @@ type Message interface { IsFake() bool Data() []byte AccessList() types.AccessList + + // In legacy transaction, this is the same as From. + // In sponsored transaction, this is the payer's + // address recovered from the payer's signature. + Payer() common.Address + ExpiredTime() uint64 } // ExecutionResult includes all output after executing given evm @@ -191,24 +198,40 @@ func (st *StateTransition) to() common.Address { } func (st *StateTransition) buyGas() error { - mgval := new(big.Int).SetUint64(st.msg.Gas()) - mgval = mgval.Mul(mgval, st.gasPrice) - balanceCheck := mgval - if st.gasFeeCap != nil { - balanceCheck = new(big.Int).SetUint64(st.msg.Gas()) - balanceCheck = balanceCheck.Mul(balanceCheck, st.gasFeeCap) - balanceCheck.Add(balanceCheck, st.value) - } - if have, want := st.state.GetBalance(st.msg.From()), balanceCheck; have.Cmp(want) < 0 { - return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From().Hex(), have, want) + gas := new(big.Int).SetUint64(st.msg.Gas()) + // In transaction types other than dynamic fee transaction, + // effectiveGasFee is the same as maxGasFee. In dynamic fee + // transaction, st.gasPrice is the already calculated gas + // price based on block base fee, gas fee cap and gas tip cap + effectiveGasFee := new(big.Int).Mul(gas, st.gasPrice) + maxGasFee := new(big.Int).Mul(gas, st.gasFeeCap) + + if st.msg.Payer() != st.msg.From() { + // This is sponsored transaction, check gas fee with payer's balance and msg.value with sender's balance + if have, want := st.state.GetBalance(st.msg.Payer()), maxGasFee; have.Cmp(want) < 0 { + return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientPayerFunds, st.msg.Payer().Hex(), have, want) + } + + if have, want := st.state.GetBalance(st.msg.From()), st.value; have.Cmp(want) < 0 { + return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientSenderFunds, st.msg.From().Hex(), have, want) + } + } else { + gasFeeAndValue := new(big.Int).Add(maxGasFee, st.value) + if have, want := st.state.GetBalance(st.msg.From()), gasFeeAndValue; have.Cmp(want) < 0 { + return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From().Hex(), have, want) + } } + if err := st.gp.SubGas(st.msg.Gas()); err != nil { return err } st.gas += st.msg.Gas() st.initialGas = st.msg.Gas() - st.state.SubBalance(st.msg.From(), mgval) + + // Subtract the gas fee from balance of the fee payer, + // the msg.value is transfered to the recipient in later step. + st.state.SubBalance(st.msg.Payer(), effectiveGasFee) return nil } @@ -257,6 +280,19 @@ func (st *StateTransition) preCheck() error { } } } + + // Check expired time, gas fee cap and tip cap in sponsored transaction + if st.msg.Payer() != st.msg.From() { + if st.msg.ExpiredTime() <= st.evm.Context.Time { + return fmt.Errorf("%w: expiredTime: %d, blockTime: %d", ErrExpiredSponsoredTx, + st.msg.ExpiredTime(), st.evm.Context.Time) + } + + if st.msg.GasTipCap().Cmp(st.msg.GasFeeCap()) != 0 { + return ErrDifferentFeeCapTipCap + } + } + return st.buyGas() } @@ -278,7 +314,8 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { // applying the message. The rules include these clauses // // 1. the nonce of the message caller is correct - // 2. caller has enough balance to cover transaction fee(gaslimit * gasprice) + // 2. payer has enough balance to cover transaction fee(gaslimit * gasprice) + // payer signature is not expired // 3. the amount of gas required is available in the block // 4. the purchased gas is enough to cover intrinsic usage // 5. there is no overflow when calculating intrinsic gas @@ -376,7 +413,7 @@ func (st *StateTransition) refundGas(refundQuotient uint64) { // Return ETH for remaining gas, exchanged at the original rate. remaining := new(big.Int).Mul(new(big.Int).SetUint64(st.gas), st.gasPrice) - st.state.AddBalance(st.msg.From(), remaining) + st.state.AddBalance(st.msg.Payer(), remaining) // Also return remaining gas to the block gas counter so it is // available for the next transaction. diff --git a/core/tx_list.go b/core/tx_list.go index f141a03bbd..c157aeb74c 100644 --- a/core/tx_list.go +++ b/core/tx_list.go @@ -253,17 +253,21 @@ type txList struct { strict bool // Whether nonces are strictly continuous or not txs *txSortedMap // Heap indexed sorted hash map of the transactions - costcap *big.Int // Price of the highest costing transaction (reset only if exceeds balance) - gascap uint64 // Gas limit of the highest spending transaction (reset only if exceeds block limit) + costcap *big.Int // Price of the highest costing transaction (reset only if exceeds balance) + gascap uint64 // Gas limit of the highest spending transaction (reset only if exceeds block limit) + signer types.Signer // The signer of the transaction pool + payers map[common.Address]int // The reference count of payers } // newTxList create a new transaction list for maintaining nonce-indexable fast, // gapped, sortable transaction lists. -func newTxList(strict bool) *txList { +func newTxList(strict bool, signer types.Signer) *txList { return &txList{ strict: strict, txs: newTxSortedMap(), costcap: new(big.Int), + signer: signer, + payers: make(map[common.Address]int), } } @@ -310,14 +314,34 @@ func (l *txList) Add(tx *types.Transaction, priceBump uint64) (bool, *types.Tran if gas := tx.Gas(); l.gascap < gas { l.gascap = gas } + payer, err := types.Payer(l.signer, tx) + if err == nil { + l.payers[payer]++ + } return true, old } +// removePayer decrease the reference count of payers in the list +// and remove the payer if the reference count reaches 0 +func (l *txList) removePayer(removed types.Transactions) { + for _, removedTx := range removed { + payer, err := types.Payer(l.signer, removedTx) + if err == nil { + l.payers[payer]-- + if l.payers[payer] == 0 { + delete(l.payers, payer) + } + } + } +} + // Forward removes all transactions from the list with a nonce lower than the // provided threshold. Every removed transaction is returned for any post-removal // maintenance. func (l *txList) Forward(threshold uint64) types.Transactions { - return l.txs.Forward(threshold) + removed := l.txs.Forward(threshold) + l.removePayer(removed) + return removed } // Filter removes all transactions from the list with a cost or gas limit higher @@ -329,17 +353,35 @@ func (l *txList) Forward(threshold uint64) types.Transactions { // a point in calculating all the costs or if the balance covers all. If the threshold // is lower than the costgas cap, the caps will be reset to a new high after removing // the newly invalidated transactions. -func (l *txList) Filter(costLimit *big.Int, gasLimit uint64) (types.Transactions, types.Transactions) { +func (l *txList) Filter( + costLimit *big.Int, + gasLimit uint64, + payerCostLimit map[common.Address]*big.Int, + currentTime uint64, +) (types.Transactions, types.Transactions) { // If all transactions are below the threshold, short circuit - if l.costcap.Cmp(costLimit) <= 0 && l.gascap <= gasLimit { + if l.costcap.Cmp(costLimit) <= 0 && l.gascap <= gasLimit && len(payerCostLimit) == 0 { return nil, nil } l.costcap = new(big.Int).Set(costLimit) // Lower the caps to the thresholds l.gascap = gasLimit - // Filter out all the transactions above the account's funds - removed := l.txs.Filter(func(tx *types.Transaction) bool { - return tx.Gas() > gasLimit || tx.Cost().Cmp(costLimit) > 0 + // Filter out all the transactions above block gas limit, above + // the payer's or sender's fund + removed := l.txs.filter(func(tx *types.Transaction) bool { + if tx.Gas() > gasLimit { + return true + } + + if tx.Type() == types.SponsoredTxType { + payer, _ := types.Payer(l.signer, tx) + gasFee := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) + return gasFee.Cmp(payerCostLimit[payer]) > 0 || + tx.Value().Cmp(costLimit) > 0 || + tx.ExpiredTime() <= currentTime + } else { + return tx.Cost().Cmp(costLimit) > 0 + } }) if len(removed) == 0 { @@ -357,13 +399,17 @@ func (l *txList) Filter(costLimit *big.Int, gasLimit uint64) (types.Transactions invalids = l.txs.filter(func(tx *types.Transaction) bool { return tx.Nonce() > lowest }) } l.txs.reheap() + l.removePayer(removed) + l.removePayer(invalids) return removed, invalids } // Cap places a hard limit on the number of items, returning all transactions // exceeding that limit. func (l *txList) Cap(threshold int) types.Transactions { - return l.txs.Cap(threshold) + removed := l.txs.Cap(threshold) + l.removePayer(removed) + return removed } // Remove deletes a transaction from the maintained list, returning whether the @@ -371,14 +417,20 @@ func (l *txList) Cap(threshold int) types.Transactions { // the deletion (strict mode only). func (l *txList) Remove(tx *types.Transaction) (bool, types.Transactions) { // Remove the transaction from the set + var removed types.Transactions nonce := tx.Nonce() if removed := l.txs.Remove(nonce); !removed { return false, nil } + removed = append(removed, tx) // In strict mode, filter out non-executable transactions if l.strict { - return true, l.txs.Filter(func(tx *types.Transaction) bool { return tx.Nonce() > nonce }) + filteredTx := l.txs.Filter(func(tx *types.Transaction) bool { return tx.Nonce() > nonce }) + removed = append(removed, filteredTx...) + l.removePayer(removed) + return true, filteredTx } + l.removePayer(removed) return true, nil } @@ -416,6 +468,17 @@ func (l *txList) LastElement() *types.Transaction { return l.txs.LastElement() } +func (l *txList) Payers() []common.Address { + payers := make([]common.Address, len(l.payers)) + i := 0 + for payer := range l.payers { + payers[i] = payer + i++ + } + + return payers +} + // priceHeap is a heap.Interface implementation over transactions for retrieving // price-sorted transactions to discard when the pool fills up. If baseFee is set // then the heap is sorted based on the effective tip based on the given base fee. diff --git a/core/tx_list_test.go b/core/tx_list_test.go index ef49cae1dd..6c483eb133 100644 --- a/core/tx_list_test.go +++ b/core/tx_list_test.go @@ -21,6 +21,7 @@ import ( "math/rand" "testing" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" ) @@ -36,7 +37,7 @@ func TestStrictTxListAdd(t *testing.T) { txs[i] = transaction(uint64(i), 0, key) } // Insert the transactions in a random order - list := newTxList(true) + list := newTxList(true, types.NewEIP155Signer(common.Big1)) for _, v := range rand.Perm(len(txs)) { list.Add(txs[v], DefaultTxPoolConfig.PriceBump) } @@ -63,10 +64,10 @@ func BenchmarkTxListAdd(b *testing.B) { priceLimit := big.NewInt(int64(DefaultTxPoolConfig.PriceLimit)) b.ResetTimer() for i := 0; i < b.N; i++ { - list := newTxList(true) + list := newTxList(true, types.NewEIP155Signer(common.Big1)) for _, v := range rand.Perm(len(txs)) { list.Add(txs[v], DefaultTxPoolConfig.PriceBump) - list.Filter(priceLimit, DefaultTxPoolConfig.PriceBump) + list.Filter(priceLimit, DefaultTxPoolConfig.PriceBump, make(map[common.Address]*big.Int), 0) } } } diff --git a/core/tx_pool.go b/core/tx_pool.go index 98e819d42a..239902c627 100644 --- a/core/tx_pool.go +++ b/core/tx_pool.go @@ -61,6 +61,9 @@ var ( // ErrInvalidSender is returned if the transaction contains an invalid signature. ErrInvalidSender = errors.New("invalid sender") + // ErrInvalidPayer is returned if the transaction contains an invalid payer signature. + ErrInvalidPayer = errors.New("invalid payer") + // ErrUnderpriced is returned if a transaction's gas price is below the minimum // configured for the transaction pool. ErrUnderpriced = errors.New("transaction underpriced") @@ -247,11 +250,13 @@ type TxPool struct { mu sync.RWMutex istanbul bool // Fork indicator whether we are in the istanbul stage. - eip2718 bool // Fork indicator whether we are using EIP-2718 type transactions. + berlin bool // Fork indicator whether we are using access list type transactions. eip1559 bool // Fork indicator whether we are using EIP-1559 type transactions. odysseus bool // Fork indicator whether we are in the Odysseus stage. antenna bool // Fork indicator whether we are in Antenna stage. + miko bool // Fork indicator whether we are using sponsored trnasactions. + currentTime uint64 // Current block time in blockchain head currentState *state.StateDB // Current state in the blockchain head pendingNonces *txNoncer // Pending state tracking virtual nonces currentMaxGas uint64 // Current gas limit for transaction caps @@ -592,14 +597,18 @@ func (pool *TxPool) local() map[common.Address]types.Transactions { // validateTx checks whether a transaction is valid according to the consensus // rules and adheres to some heuristic limits of the local node (price and size). func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error { - // Accept only legacy transactions until EIP-2718/2930 activates. - if !pool.eip2718 && tx.Type() != types.LegacyTxType { + // Reject access list transactions until Berlin activates. + if !pool.berlin && tx.Type() == types.AccessListTxType { return ErrTxTypeNotSupported } // Reject dynamic fee transactions until EIP-1559 activates. if !pool.eip1559 && tx.Type() == types.DynamicFeeTxType { return ErrTxTypeNotSupported } + // Reject sponsored transactions until Miko hardfork. + if !pool.miko && tx.Type() == types.SponsoredTxType { + return ErrTxTypeNotSupported + } // Reject transactions over defined size to prevent DOS attacks if uint64(tx.Size()) > txMaxSize { return ErrOversizedData @@ -642,10 +651,46 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error { if pool.currentState.GetNonce(from) > tx.Nonce() { return ErrNonceTooLow } - // Transactor should have enough funds to cover the costs - // cost == V + GP * GL - if pool.currentState.GetBalance(from).Cmp(tx.Cost()) < 0 { - return ErrInsufficientFunds + payer := from + if tx.Type() == types.SponsoredTxType { + // Currently, these 2 fields must be the same in sponsored transaction. + // We create 2 separate fields to reserve for the future, in case we + // decide to support dynamic fee transaction. + if tx.GasFeeCap().Cmp(tx.GasTipCap()) != 0 { + return ErrDifferentFeeCapTipCap + } + + // Ensure sponsored transaction is not expired + if tx.ExpiredTime() <= pool.currentTime { + return ErrExpiredSponsoredTx + } + + addr, err := types.Payer(pool.signer, tx) + if err != nil { + return ErrInvalidPayer + } + payer = addr + // Ensure payer is different from sender + if payer == from { + return types.ErrSamePayerSenderSponsoredTx + } + + // Ensure payer can pay for the gas fee == gas fee cap * gas limit + gasFee := new(big.Int).Mul(tx.GasFeeCap(), new(big.Int).SetUint64(tx.Gas())) + if pool.currentState.GetBalance(payer).Cmp(gasFee) < 0 { + return ErrInsufficientPayerFunds + } + + // Ensure sender can pay for the value + if pool.currentState.GetBalance(from).Cmp(tx.Value()) < 0 { + return ErrInsufficientSenderFunds + } + } else { + // Sender should have enough funds to cover the costs + // cost == V + GP * GL + if pool.currentState.GetBalance(from).Cmp(tx.Cost()) < 0 { + return ErrInsufficientFunds + } } // Ensure the transaction has more gas than the basic tx fee. intrGas, err := IntrinsicGas(tx.Data(), tx.AccessList(), tx.To() == nil, true, pool.istanbul) @@ -663,7 +708,7 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error { whitelisted = state.IsWhitelistedDeployerV2( pool.currentState, from, - pool.chain.CurrentBlock().Time(), + pool.currentTime, pool.chainconfig.WhiteListDeployerContractV2Address, ) } else { @@ -677,7 +722,9 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error { // Check if sender and recipient are blacklisted if pool.chainconfig.Consortium != nil && pool.odysseus { contractAddr := pool.chainconfig.BlacklistContractAddress - if state.IsAddressBlacklisted(pool.currentState, contractAddr, &from) || state.IsAddressBlacklisted(pool.currentState, contractAddr, tx.To()) { + if state.IsAddressBlacklisted(pool.currentState, contractAddr, &from) || + state.IsAddressBlacklisted(pool.currentState, contractAddr, tx.To()) || + state.IsAddressBlacklisted(pool.currentState, contractAddr, &payer) { return ErrAddressBlacklisted } } @@ -796,7 +843,7 @@ func (pool *TxPool) enqueueTx(hash common.Hash, tx *types.Transaction, local boo // Try to insert the transaction into the future queue from, _ := types.Sender(pool.signer, tx) // already validated if pool.queue[from] == nil { - pool.queue[from] = newTxList(false) + pool.queue[from] = newTxList(false, pool.signer) } inserted, old := pool.queue[from].Add(tx, pool.config.PriceBump) if !inserted { @@ -848,7 +895,7 @@ func (pool *TxPool) journalTx(from common.Address, tx *types.Transaction) { func (pool *TxPool) promoteTx(addr common.Address, hash common.Hash, tx *types.Transaction) bool { // Try to insert the transaction into the pending queue if pool.pending[addr] == nil { - pool.pending[addr] = newTxList(true) + pool.pending[addr] = newTxList(true, pool.signer) } list := pool.pending[addr] @@ -1333,6 +1380,7 @@ func (pool *TxPool) reset(oldHead, newHead *types.Header) { log.Error("Failed to reset txpool state", "err", err) return } + pool.currentTime = newHead.Time pool.currentState = statedb pool.pendingNonces = newTxNoncer(statedb) pool.currentMaxGas = newHead.GasLimit @@ -1345,10 +1393,11 @@ func (pool *TxPool) reset(oldHead, newHead *types.Header) { // Update all fork indicator by next pending block number. next := new(big.Int).Add(newHead.Number, big.NewInt(1)) pool.istanbul = pool.chainconfig.IsIstanbul(next) - pool.eip2718 = pool.chainconfig.IsBerlin(next) + pool.berlin = pool.chainconfig.IsBerlin(next) pool.eip1559 = pool.chainconfig.IsLondon(next) pool.odysseus = pool.chainconfig.IsOdysseus(next) pool.antenna = pool.chainconfig.IsAntenna(next) + pool.miko = pool.chainconfig.IsMiko(next) } // promoteExecutables moves transactions that have become processable from the @@ -1371,8 +1420,14 @@ func (pool *TxPool) promoteExecutables(accounts []common.Address) []*types.Trans pool.all.Remove(hash) } log.Trace("Removed old queued transactions", "count", len(forwards)) + payers := list.Payers() + payerCostLimit := make(map[common.Address]*big.Int) + for _, payer := range payers { + payerCostLimit[payer] = pool.currentState.GetBalance(payer) + } + // Drop all transactions that are too costly (low balance or out of gas) - drops, _ := list.Filter(pool.currentState.GetBalance(addr), pool.currentMaxGas) + drops, _ := list.Filter(pool.currentState.GetBalance(addr), pool.currentMaxGas, payerCostLimit, pool.currentTime) for _, tx := range drops { hash := tx.Hash() pool.all.Remove(hash) @@ -1568,8 +1623,14 @@ func (pool *TxPool) demoteUnexecutables() { pool.all.Remove(hash) log.Trace("Removed old pending transaction", "hash", hash) } + payers := list.Payers() + payerCostLimit := make(map[common.Address]*big.Int) + for _, payer := range payers { + payerCostLimit[payer] = pool.currentState.GetBalance(payer) + } + // Drop all transactions that are too costly (low balance or out of gas), and queue any invalids back for later - drops, invalids := list.Filter(pool.currentState.GetBalance(addr), pool.currentMaxGas) + drops, invalids := list.Filter(pool.currentState.GetBalance(addr), pool.currentMaxGas, payerCostLimit, pool.currentTime) for _, tx := range drops { hash := tx.Hash() log.Trace("Removed unpayable pending transaction", "hash", hash) diff --git a/core/tx_pool_test.go b/core/tx_pool_test.go index a7af275835..dacbdf324d 100644 --- a/core/tx_pool_test.go +++ b/core/tx_pool_test.go @@ -34,6 +34,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" ) @@ -53,6 +54,7 @@ func init() { cpy := *params.TestChainConfig eip1559Config = &cpy + eip1559Config.MikoBlock = common.Big0 eip1559Config.BerlinBlock = common.Big0 eip1559Config.LondonBlock = common.Big0 } @@ -2428,18 +2430,73 @@ func BenchmarkPendingDemotion1000(b *testing.B) { benchmarkPendingDemotion(b, 1 func BenchmarkPendingDemotion10000(b *testing.B) { benchmarkPendingDemotion(b, 10000) } func benchmarkPendingDemotion(b *testing.B, size int) { + log.Root().SetHandler(log.DiscardHandler()) // Add a batch of transactions to a pool one by one pool, key := setupTxPool() defer pool.Stop() account := crypto.PubkeyToAddress(key.PublicKey) - testAddBalance(pool, account, big.NewInt(1000000)) + testAddBalance(pool, account, big.NewInt(100100)) for i := 0; i < size; i++ { tx := transaction(uint64(i), 100000, key) pool.promoteTx(account, tx.Hash(), tx) } // Benchmark the speed of pool validation + for i := 0; i < b.N; i++ { + // Force the txList filter to loop through the whole list + pool.pending[account].costcap = big.NewInt(200000) + pool.demoteUnexecutables() + } +} + +func BenchmarkPendingSponsoredTxDemotion100(b *testing.B) { + benchmarkPendingSponsoredTxDemotion(b, 100) +} +func BenchmarkPendingSponsoredTxDemotion1000(b *testing.B) { + benchmarkPendingSponsoredTxDemotion(b, 1000) +} +func BenchmarkPendingSponsoredTxDemotion10000(b *testing.B) { + benchmarkPendingSponsoredTxDemotion(b, 10000) +} + +func benchmarkPendingSponsoredTxDemotion(b *testing.B, size int) { + log.Root().SetHandler(log.DiscardHandler()) + // Add a batch of transactions to a pool one by one + pool, key := setupTxPool() + defer pool.Stop() + + account := crypto.PubkeyToAddress(key.PublicKey) + testAddBalance(pool, account, big.NewInt(1000000)) + mikoSigner := types.NewMikoSigner(common.Big1) + + for i := 0; i < size; i++ { + // create different payer for every transaction, worst case scenario + payerKey, _ := crypto.GenerateKey() + testAddBalance(pool, crypto.PubkeyToAddress(payerKey.PublicKey), big.NewInt(1000000)) + + innerTx := types.SponsoredTx{ + ChainID: common.Big1, + Nonce: uint64(i), + GasTipCap: common.Big1, + GasFeeCap: common.Big1, + Gas: 21000, + To: &account, + ExpiredTime: 100, + } + var err error + innerTx.PayerR, innerTx.PayerS, innerTx.PayerV, err = types.PayerSign(payerKey, mikoSigner, account, &innerTx) + if err != nil { + b.Fatal(err) + } + + tx, err := types.SignNewTx(key, mikoSigner, &innerTx) + if err != nil { + b.Fatal(err) + } + pool.promoteTx(account, tx.Hash(), tx) + } + // Benchmark the speed of pool validation b.ResetTimer() for i := 0; i < b.N; i++ { pool.demoteUnexecutables() @@ -2481,6 +2538,7 @@ func BenchmarkPoolBatchLocalInsert1000(b *testing.B) { benchmarkPoolBatchInsert func BenchmarkPoolBatchLocalInsert10000(b *testing.B) { benchmarkPoolBatchInsert(b, 10000, true) } func benchmarkPoolBatchInsert(b *testing.B, size int, local bool) { + log.Root().SetHandler(log.DiscardHandler()) // Generate a batch of transactions to enqueue into the pool pool, key := setupTxPool() defer pool.Stop() @@ -2506,6 +2564,64 @@ func benchmarkPoolBatchInsert(b *testing.B, size int, local bool) { } } +func BenchmarkPoolBatchInsertSponsoredTx100(b *testing.B) { + benchmarkPoolBatchInsertSponsoredTx(b, 100) +} +func BenchmarkPoolBatchInsertSponsoredTx1000(b *testing.B) { + benchmarkPoolBatchInsertSponsoredTx(b, 1000) +} +func BenchmarkPoolBatchInsertSponsoredTx10000(b *testing.B) { + benchmarkPoolBatchInsertSponsoredTx(b, 10000) +} + +func benchmarkPoolBatchInsertSponsoredTx(b *testing.B, size int) { + log.Root().SetHandler(log.DiscardHandler()) + // Add a batch of transactions to a pool one by one + pool, key := setupTxPool() + defer pool.Stop() + + account := crypto.PubkeyToAddress(key.PublicKey) + testAddBalance(pool, account, big.NewInt(1000000)) + mikoSigner := types.NewMikoSigner(common.Big1) + + batches := make([]types.Transactions, b.N) + for i := 0; i < b.N; i++ { + batches[i] = make(types.Transactions, size) + for j := 0; j < size; j++ { + // create different payer for every transaction, worst case scenario + payerKey, _ := crypto.GenerateKey() + testAddBalance(pool, crypto.PubkeyToAddress(payerKey.PublicKey), big.NewInt(1000000)) + + innerTx := types.SponsoredTx{ + ChainID: common.Big1, + Nonce: uint64(i), + GasTipCap: common.Big1, + GasFeeCap: common.Big1, + Gas: 21000, + To: &account, + ExpiredTime: 100, + } + var err error + innerTx.PayerR, innerTx.PayerS, innerTx.PayerV, err = types.PayerSign(payerKey, mikoSigner, account, &innerTx) + if err != nil { + b.Fatal(err) + } + + tx, err := types.SignNewTx(key, mikoSigner, &innerTx) + if err != nil { + b.Fatal(err) + } + batches[i][j] = tx + } + } + + // Benchmark the speed of pool validation + b.ResetTimer() + for _, batch := range batches { + pool.AddRemotes(batch) + } +} + func BenchmarkInsertRemoteWithAllLocals(b *testing.B) { // Allocate keys for testing key, _ := crypto.GenerateKey() @@ -2561,3 +2677,414 @@ func BenchmarkPoolMultiAccountBatchInsert(b *testing.B) { pool.AddRemotesSync([]*types.Transaction{tx}) } } + +func TestSponsoredTxBeforeMiko(t *testing.T) { + var chainConfig params.ChainConfig + + chainConfig.EIP155Block = common.Big0 + chainConfig.ChainID = big.NewInt(2020) + + recipient := common.HexToAddress("1000000000000000000000000000000000000001") + txpool, senderKey := setupTxPoolWithConfig(&chainConfig) + defer txpool.Stop() + + payerKey, err := crypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + + innerTx := types.SponsoredTx{ + ChainID: big.NewInt(2020), + Nonce: 1, + GasTipCap: big.NewInt(100000), + GasFeeCap: big.NewInt(100000), + Gas: 1000, + To: &recipient, + Value: big.NewInt(10), + Data: []byte("abcd"), + ExpiredTime: 100000, + } + + mikoSigner := types.NewMikoSigner(big.NewInt(2020)) + innerTx.PayerR, innerTx.PayerS, innerTx.PayerV, err = types.PayerSign( + payerKey, + mikoSigner, + crypto.PubkeyToAddress(senderKey.PublicKey), + &innerTx, + ) + if err != nil { + t.Fatalf("Payer fails to sign transaction, err %s", err) + } + + tx, err := types.SignNewTx(senderKey, mikoSigner, &innerTx) + if err != nil { + t.Fatalf("Fail to sign transaction, err %s", err) + } + + err = txpool.AddRemote(tx) + if err == nil || !errors.Is(err, ErrInvalidSender) { + t.Fatalf("Expect error %s, get %s", ErrInvalidSender, err) + } +} + +func TestExpiredTimeAndGasCheckSponsoredTx(t *testing.T) { + var chainConfig params.ChainConfig + + chainConfig.EIP155Block = common.Big0 + chainConfig.MikoBlock = common.Big0 + chainConfig.ChainID = big.NewInt(2020) + + recipient := common.HexToAddress("1000000000000000000000000000000000000001") + statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) + blockchain := &testBlockChain{10000000, statedb, new(event.Feed)} + + txpool := NewTxPool(testTxPoolConfig, &chainConfig, blockchain) + defer txpool.Stop() + + senderKey, err := crypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + + payerKey, err := crypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + + innerTx := types.SponsoredTx{ + ChainID: big.NewInt(2020), + Nonce: 1, + GasTipCap: big.NewInt(100000), + GasFeeCap: big.NewInt(200000), + Gas: 22000, + To: &recipient, + Value: big.NewInt(10), + Data: []byte("abcd"), + ExpiredTime: 100, + } + + mikoSigner := types.NewMikoSigner(big.NewInt(2020)) + innerTx.PayerR, innerTx.PayerS, innerTx.PayerV, err = types.PayerSign( + payerKey, + mikoSigner, + crypto.PubkeyToAddress(senderKey.PublicKey), + &innerTx, + ) + if err != nil { + t.Fatalf("Payer fails to sign transaction, err %s", err) + } + + tx, err := types.SignNewTx(senderKey, mikoSigner, &innerTx) + if err != nil { + t.Fatalf("Fail to sign transaction, err %s", err) + } + + // 1. Failed when gas tip cap and gas tip cap are different + err = txpool.addRemoteSync(tx) + if err == nil || !errors.Is(err, ErrDifferentFeeCapTipCap) { + t.Fatalf("Expect error %s, get %s", ErrDifferentFeeCapTipCap, err) + } + + // 2. Failed when tx is expired + txpool.currentTime = 2000 + innerTx.GasFeeCap = innerTx.GasTipCap + innerTx.PayerR, innerTx.PayerS, innerTx.PayerV, err = types.PayerSign( + payerKey, + mikoSigner, + crypto.PubkeyToAddress(senderKey.PublicKey), + &innerTx, + ) + if err != nil { + t.Fatalf("Payer fails to sign transaction, err %s", err) + } + + tx, err = types.SignNewTx(senderKey, mikoSigner, &innerTx) + if err != nil { + t.Fatalf("Fail to sign transaction, err %s", err) + } + + err = txpool.addRemoteSync(tx) + if err == nil || !errors.Is(err, ErrExpiredSponsoredTx) { + t.Fatalf("Expect error %s, get %s", ErrExpiredSponsoredTx, err) + } + + // 3. Failed when sponsored tx has the same payer and sender + innerTx.ExpiredTime = 3000 + innerTx.PayerR, innerTx.PayerS, innerTx.PayerV, err = types.PayerSign( + payerKey, + mikoSigner, + crypto.PubkeyToAddress(payerKey.PublicKey), + &innerTx, + ) + if err != nil { + t.Fatalf("Payer fails to sign transaction, err %s", err) + } + + tx, err = types.SignNewTx(payerKey, mikoSigner, &innerTx) + if err != nil { + t.Fatalf("Fail to sign transaction, err %s", err) + } + + err = txpool.addRemoteSync(tx) + if err == nil || !errors.Is(err, types.ErrSamePayerSenderSponsoredTx) { + t.Fatalf("Expect error %s, get %s", types.ErrSamePayerSenderSponsoredTx, err) + } + + // 4. Failed when payer does not have sufficient fund for gas fee + innerTx.PayerR, innerTx.PayerS, innerTx.PayerV, err = types.PayerSign( + payerKey, + mikoSigner, + crypto.PubkeyToAddress(senderKey.PublicKey), + &innerTx, + ) + if err != nil { + t.Fatalf("Payer fails to sign transaction, err %s", err) + } + + tx, err = types.SignNewTx(senderKey, mikoSigner, &innerTx) + if err != nil { + t.Fatalf("Fail to sign transaction, err %s", err) + } + + err = txpool.addRemoteSync(tx) + if err == nil || !errors.Is(err, ErrInsufficientPayerFunds) { + t.Fatalf("Expect error %s, get %s", ErrInsufficientPayerFunds, err) + } + + // 5. Failed when sender does not have sufficient fund for msg.value + statedb.SetBalance(crypto.PubkeyToAddress(payerKey.PublicKey), new(big.Int).Mul(big.NewInt(100000), big.NewInt(22000))) + err = txpool.addRemoteSync(tx) + if err == nil || !errors.Is(err, ErrInsufficientSenderFunds) { + t.Fatalf("Expect error %s, get %s", ErrInsufficientSenderFunds, err) + } + + // 6. Successfully add tx + statedb.SetBalance(crypto.PubkeyToAddress(senderKey.PublicKey), big.NewInt(10)) + err = txpool.addRemoteSync(tx) + if err != nil { + t.Fatalf("Expect successfully add tx, get %s", err) + } +} + +// TestSponsoredTxInTxPoolQueue tests that sponsored tx is removed from +// txpool's queue when balance of payer/sender is insufficient or tx +// is expired +func TestSponsoredTxInTxPoolQueue(t *testing.T) { + var chainConfig params.ChainConfig + + chainConfig.EIP155Block = common.Big0 + chainConfig.MikoBlock = common.Big0 + chainConfig.ChainID = big.NewInt(2020) + + recipient := common.HexToAddress("1000000000000000000000000000000000000001") + statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) + blockchain := &testBlockChain{10000000, statedb, new(event.Feed)} + + txpool := NewTxPool(testTxPoolConfig, &chainConfig, blockchain) + defer txpool.Stop() + + senderKey, err := crypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + senderAddr := crypto.PubkeyToAddress(senderKey.PublicKey) + + payerKey, err := crypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + payerAddr := crypto.PubkeyToAddress(payerKey.PublicKey) + + sponsoredTx1 := types.SponsoredTx{ + ChainID: big.NewInt(2020), + Nonce: 0, + GasTipCap: big.NewInt(100000), + GasFeeCap: big.NewInt(100000), + Gas: 30000, + To: &recipient, + Value: big.NewInt(10), + ExpiredTime: 100, + } + sponsoredTx2 := types.SponsoredTx{ + ChainID: big.NewInt(2020), + Nonce: 2, + GasTipCap: big.NewInt(100000), + GasFeeCap: big.NewInt(100000), + Gas: 30000, + To: &recipient, + Value: big.NewInt(10), + ExpiredTime: 100, + } + gasFee := new(big.Int).Mul(sponsoredTx1.GasFeeCap, new(big.Int).SetUint64(sponsoredTx1.Gas)) + statedb.SetBalance(payerAddr, gasFee) + statedb.SetBalance(senderAddr, sponsoredTx1.Value) + + mikoSigner := types.NewMikoSigner(big.NewInt(2020)) + sponsoredTx1.PayerR, sponsoredTx1.PayerS, sponsoredTx1.PayerV, err = types.PayerSign( + payerKey, + mikoSigner, + crypto.PubkeyToAddress(senderKey.PublicKey), + &sponsoredTx1, + ) + if err != nil { + t.Fatalf("Payer fails to sign transaction, err %s", err) + } + + tx1, err := types.SignNewTx(senderKey, mikoSigner, &sponsoredTx1) + if err != nil { + t.Fatalf("Fail to sign transaction, err %s", err) + } + + sponsoredTx2.PayerR, sponsoredTx2.PayerS, sponsoredTx2.PayerV, err = types.PayerSign( + payerKey, + mikoSigner, + crypto.PubkeyToAddress(senderKey.PublicKey), + &sponsoredTx2, + ) + if err != nil { + t.Fatalf("Payer fails to sign transaction, err %s", err) + } + + tx2, err := types.SignNewTx(senderKey, mikoSigner, &sponsoredTx2) + if err != nil { + t.Fatalf("Fail to sign transaction, err %s", err) + } + + errs := txpool.AddRemotesSync([]*types.Transaction{tx1, tx2}) + for _, err := range errs { + if err != nil { + t.Fatalf("Fail to add tx to pool, err %s", err) + } + } + + pending, queued := txpool.Stats() + if pending != 1 { + t.Fatalf("Pending txpool, expect %d get %d", 1, pending) + } + if queued != 1 { + t.Fatalf("Queued txpool, expect %d get %d", 1, queued) + } + + // 1. Payer fund is insufficient, 2 txs are removed from pending and queued + statedb.SubBalance(payerAddr, common.Big1) + <-txpool.requestReset(nil, nil) + pending, queued = txpool.Stats() + if pending != 0 { + t.Fatalf("Pending txpool, expect %d get %d", 0, pending) + } + if queued != 0 { + t.Fatalf("Queued txpool, expect %d get %d", 0, queued) + } + + // 2. Sender fund is insufficient, 2 txs are removed from pending and queued + statedb.AddBalance(payerAddr, common.Big1) + errs = txpool.AddRemotesSync([]*types.Transaction{tx1, tx2}) + for _, err := range errs { + if err != nil { + t.Fatalf("Fail to add tx to pool, err %s", err) + } + } + + pending, queued = txpool.Stats() + if pending != 1 { + t.Fatalf("Pending txpool, expect %d get %d", 1, pending) + } + if queued != 1 { + t.Fatalf("Queued txpool, expect %d get %d", 1, queued) + } + + statedb.SubBalance(senderAddr, common.Big1) + <-txpool.requestReset(nil, nil) + pending, queued = txpool.Stats() + if pending != 0 { + t.Fatalf("Pending txpool, expect %d get %d", 0, pending) + } + if queued != 0 { + t.Fatalf("Queued txpool, expect %d get %d", 0, queued) + } + + // 3. Payer fund is insufficient, 2 txs with the same payer in a queue + statedb.AddBalance(senderAddr, common.Big1) + sponsoredTx3 := types.SponsoredTx{ + ChainID: big.NewInt(2020), + Nonce: 3, + GasTipCap: big.NewInt(100000), + GasFeeCap: big.NewInt(100000), + Gas: 21000, + To: &recipient, + Value: big.NewInt(10), + ExpiredTime: 100, + } + + sponsoredTx3.PayerR, sponsoredTx3.PayerS, sponsoredTx3.PayerV, err = types.PayerSign( + payerKey, + mikoSigner, + crypto.PubkeyToAddress(senderKey.PublicKey), + &sponsoredTx3, + ) + if err != nil { + t.Fatalf("Payer fails to sign transaction, err %s", err) + } + + tx3, err := types.SignNewTx(senderKey, mikoSigner, &sponsoredTx3) + if err != nil { + t.Fatalf("Fail to sign transaction, err %s", err) + } + + errs = txpool.AddRemotesSync([]*types.Transaction{tx2, tx3}) + for _, err := range errs { + if err != nil { + t.Fatalf("Fail to add tx to pool, err %s", err) + } + } + + _, queued = txpool.Stats() + if queued != 2 { + t.Fatalf("Queued txpool, expect %d get %d", 2, queued) + } + + gasFee = new(big.Int).Mul(sponsoredTx3.GasFeeCap, new(big.Int).SetUint64(sponsoredTx3.Gas)) + statedb.SetBalance(payerAddr, gasFee) + <-txpool.requestReset(nil, nil) + + // tx2 must be removed from queue but not tx3 + _, queued = txpool.Stats() + if queued != 1 { + t.Fatalf("Queued txpool, expect %d get %d", 1, queued) + } + + statedb.SubBalance(payerAddr, common.Big1) + <-txpool.requestReset(nil, nil) + // tx3 must be removed now + _, queued = txpool.Stats() + if queued != 0 { + t.Fatalf("Queued txpool, expect %d get %d", 0, queued) + } + + // 4. Expired txs are removed from pending and queued + gasFee = new(big.Int).Mul(sponsoredTx1.GasFeeCap, new(big.Int).SetUint64(sponsoredTx1.Gas)) + statedb.SetBalance(payerAddr, gasFee) + errs = txpool.AddRemotesSync([]*types.Transaction{tx1, tx2}) + for _, err := range errs { + if err != nil { + t.Fatalf("Fail to add tx to pool, err %s", err) + } + } + + pending, queued = txpool.Stats() + if pending != 1 { + t.Fatalf("Pending txpool, expect %d get %d", 1, pending) + } + if queued != 1 { + t.Fatalf("Queued txpool, expect %d get %d", 1, queued) + } + + <-txpool.requestReset(nil, types.CopyHeader(&types.Header{Time: 200})) + pending, queued = txpool.Stats() + if pending != 0 { + t.Fatalf("Pending txpool, expect %d get %d", 0, pending) + } + if queued != 0 { + t.Fatalf("Queued txpool, expect %d get %d", 0, queued) + } +}