From b005fa41c8bda864d94ab617f463aa438a909c1e Mon Sep 17 00:00:00 2001 From: Bui Quang Minh Date: Thu, 26 Oct 2023 17:35:53 +0700 Subject: [PATCH] core/state, txpool: handle new sponsored transaction, adjust gas check This commit adjust the gas check to correctly check gas fee from payer of transaction and value from sender in txpool and in preCheck before applying transaction. This commit also enables EIP-2718 to support typed transaction envelop in txpool after Miko hardfork without enabling Berlin hardfork. --- core/blockchain_test.go | 269 +++++++++++++++++++- core/error.go | 15 ++ core/state_processor.go | 5 +- core/state_transition.go | 65 +++-- core/tx_list.go | 85 ++++++- core/tx_list_test.go | 7 +- core/tx_pool.go | 89 +++++-- core/tx_pool_test.go | 529 ++++++++++++++++++++++++++++++++++++++- 8 files changed, 1019 insertions(+), 45 deletions(-) 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) + } +}