From d485e8e55ee8fc865c8c7464a9ee9e3b41a7c939 Mon Sep 17 00:00:00 2001 From: minh-bq <97180373+minh-bq@users.noreply.github.com> Date: Mon, 20 Nov 2023 11:11:14 +0700 Subject: [PATCH] tx_pool, state_transition: accept sponsored tx with expired time = 0 (#379) * tx_pool, state_transition: accept sponsored tx with expired time = 0 The sponsored transaction with expired time = 0 means there is no expired time for that transaction, so we skip the expired time check for that one. * api: add payer field to sponsored transaction in RPC response This commit adds payer field to sponsored transaction in RPC response of APIs related to transactions to easy lookup without manually recovering from the returned payer's signature. * txpool: update the signer of txlist when it differs from txpool's signer txpool's signer can change when we reach higher block that triggers the new signer. This commit checks the txpool's and txlist's signer when adding the new transaction to txlist, update the txlist's signer with txpool's one when they differ. * state_transition: nil check gasFeeCap before using in buyGas In normal case, gasFeeCap is never nil. In transactions other than dynamic fee transaction, gasFeeCap is set to gasPrice. However, in some test cases, gasFeeCap can be nil. In these cases, nil check the gasFeeCap and use gasPrice instead. --- core/blockchain_test.go | 18 +++++++++++- core/state_transition.go | 10 +++++-- core/tx_list.go | 18 ++++++++++-- core/tx_pool.go | 11 +++++++- core/tx_pool_test.go | 61 ++++++++++++++++++++++++++++++++++++++++ internal/ethapi/api.go | 3 ++ 6 files changed, 115 insertions(+), 6 deletions(-) diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 8c5e912c58..04a6cac889 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -3574,7 +3574,23 @@ func TestSponsoredTxTransition(t *testing.T) { t.Fatalf("Expect error %s, get %s", ErrInsufficientSenderFunds, err) } - // 5. Successfully add tx + // 5. Successfully add tx, sponsored tx with expired time = 0 is accepted + innerTx.ExpiredTime = 0 + 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) + } + 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 { diff --git a/core/state_transition.go b/core/state_transition.go index 90b6484104..4e9a5d9aa4 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -204,7 +204,12 @@ func (st *StateTransition) buyGas() error { // 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) + var maxGasFee *big.Int + if st.gasFeeCap != nil { + maxGasFee = new(big.Int).Mul(gas, st.gasFeeCap) + } else { + maxGasFee = new(big.Int).Mul(gas, st.gasPrice) + } 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 @@ -283,7 +288,8 @@ 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 { + expiredTime := st.msg.ExpiredTime() + if expiredTime != 0 && expiredTime <= st.evm.Context.Time { return fmt.Errorf("%w: expiredTime: %d, blockTime: %d", ErrExpiredSponsoredTx, st.msg.ExpiredTime(), st.evm.Context.Time) } diff --git a/core/tx_list.go b/core/tx_list.go index c157aeb74c..924ced4c09 100644 --- a/core/tx_list.go +++ b/core/tx_list.go @@ -271,6 +271,16 @@ func newTxList(strict bool, signer types.Signer) *txList { } } +// Signer returns the signer of txlist +func (l *txList) Signer() types.Signer { + return l.signer +} + +// The txpool's signer has changed, we need to update the signer of txlist +func (l *txList) UpdateSigner(signer types.Signer) { + l.signer = signer +} + // Overlaps returns whether the transaction specified has the same nonce as one // already contained within the list. func (l *txList) Overlaps(tx *types.Transaction) bool { @@ -374,11 +384,15 @@ func (l *txList) Filter( } if tx.Type() == types.SponsoredTxType { - payer, _ := types.Payer(l.signer, tx) + payer, err := types.Payer(l.signer, tx) + if err != nil { + return false + } gasFee := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) + expiredTime := tx.ExpiredTime() return gasFee.Cmp(payerCostLimit[payer]) > 0 || tx.Value().Cmp(costLimit) > 0 || - tx.ExpiredTime() <= currentTime + (expiredTime != 0 && expiredTime <= currentTime) } else { return tx.Cost().Cmp(costLimit) > 0 } diff --git a/core/tx_pool.go b/core/tx_pool.go index 239902c627..698cef4d5e 100644 --- a/core/tx_pool.go +++ b/core/tx_pool.go @@ -661,7 +661,8 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error { } // Ensure sponsored transaction is not expired - if tx.ExpiredTime() <= pool.currentTime { + expiredTime := tx.ExpiredTime() + if expiredTime != 0 && expiredTime <= pool.currentTime { return ErrExpiredSponsoredTx } @@ -794,6 +795,10 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e // Try to replace an existing transaction in the pending pool from, _ := types.Sender(pool.signer, tx) // already validated if list := pool.pending[from]; list != nil && list.Overlaps(tx) { + if !pool.signer.Equal(pool.pending[from].Signer()) { + pool.pending[from].UpdateSigner(pool.signer) + } + // Nonce already pending, check if required price bump is met inserted, old := list.Add(tx, pool.config.PriceBump) if !inserted { @@ -844,6 +849,8 @@ func (pool *TxPool) enqueueTx(hash common.Hash, tx *types.Transaction, local boo from, _ := types.Sender(pool.signer, tx) // already validated if pool.queue[from] == nil { pool.queue[from] = newTxList(false, pool.signer) + } else if !pool.signer.Equal(pool.queue[from].Signer()) { + pool.queue[from].UpdateSigner(pool.signer) } inserted, old := pool.queue[from].Add(tx, pool.config.PriceBump) if !inserted { @@ -896,6 +903,8 @@ func (pool *TxPool) promoteTx(addr common.Address, hash common.Hash, tx *types.T // Try to insert the transaction into the pending queue if pool.pending[addr] == nil { pool.pending[addr] = newTxList(true, pool.signer) + } else if !pool.signer.Equal(pool.pending[addr].Signer()) { + pool.pending[addr].UpdateSigner(pool.signer) } list := pool.pending[addr] diff --git a/core/tx_pool_test.go b/core/tx_pool_test.go index dacbdf324d..a696ec034c 100644 --- a/core/tx_pool_test.go +++ b/core/tx_pool_test.go @@ -2864,6 +2864,29 @@ func TestExpiredTimeAndGasCheckSponsoredTx(t *testing.T) { if err != nil { t.Fatalf("Expect successfully add tx, get %s", err) } + + // 7. Sponsored tx with expired time == 0 is accepted + innerTx.ExpiredTime = 0 + innerTx.Nonce = 2 + 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 { + t.Fatalf("Expect successfully add tx, get %s", err) + } } // TestSponsoredTxInTxPoolQueue tests that sponsored tx is removed from @@ -3087,4 +3110,42 @@ func TestSponsoredTxInTxPoolQueue(t *testing.T) { if queued != 0 { t.Fatalf("Queued txpool, expect %d get %d", 0, queued) } + + // 5. Sponsored tx with expired time == 0 is not removed + <-txpool.requestReset(nil, types.CopyHeader(&types.Header{Time: 200, GasLimit: blockchain.gasLimit})) + + sponsoredTx1.ExpiredTime = 0 + 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) + } + + errs = txpool.AddRemotesSync([]*types.Transaction{tx1}) + for _, err := range errs { + if err != nil { + t.Fatalf("Fail to add tx to pool, err %s", err) + } + } + + pending, _ = txpool.Stats() + if pending != 1 { + t.Fatalf("Pending txpool, expect %d get %d", 1, pending) + } + + <-txpool.requestReset(nil, types.CopyHeader(&types.Header{Time: 200, GasLimit: blockchain.gasLimit})) + + pending, _ = txpool.Stats() + if pending != 1 { + t.Fatalf("Pending txpool, expect %d get %d", 1, pending) + } } diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 04db451ceb..4679b19303 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1333,6 +1333,7 @@ type RPCTransaction struct { V *hexutil.Big `json:"v"` R *hexutil.Big `json:"r"` S *hexutil.Big `json:"s"` + Payer *common.Address `json:"payer,omitempty"` ExpiredTime *hexutil.Uint64 `json:"expiredTime,omitempty"` PayerV *hexutil.Big `json:"payerV,omitempty"` PayerR *hexutil.Big `json:"payerR,omitempty"` @@ -1384,6 +1385,7 @@ func newRPCTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber result.GasPrice = (*hexutil.Big)(tx.GasFeeCap()) } case types.SponsoredTxType: + payer, _ := types.Payer(signer, tx) result.ChainID = (*hexutil.Big)(tx.ChainId()) result.GasFeeCap = (*hexutil.Big)(tx.GasFeeCap()) result.GasTipCap = (*hexutil.Big)(tx.GasTipCap()) @@ -1401,6 +1403,7 @@ func newRPCTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber result.PayerR = (*hexutil.Big)(r) result.PayerS = (*hexutil.Big)(s) result.PayerV = (*hexutil.Big)(v) + result.Payer = &payer } return result }