diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go index 5fd3f4600a..15e865b3cf 100644 --- a/accounts/abi/bind/backends/simulated.go +++ b/accounts/abi/bind/backends/simulated.go @@ -811,6 +811,10 @@ func (m callMsg) Value() *big.Int { return m.CallMsg.Value } func (m callMsg) Data() []byte { return m.CallMsg.Data } func (m callMsg) AccessList() types.AccessList { return m.CallMsg.AccessList } +// FIXME: support sponsored transaction in callMsg +func (m callMsg) Payer() common.Address { return m.CallMsg.From } +func (m callMsg) ExpiredTime() uint64 { return 0 } + // filterBackend implements filters.Backend to support filtering for logs without // taking bloom-bits acceleration structures into account. type filterBackend struct { 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) + } +} diff --git a/core/types/access_list_tx.go b/core/types/access_list_tx.go index ee5f194b77..90ea043cb2 100644 --- a/core/types/access_list_tx.go +++ b/core/types/access_list_tx.go @@ -105,6 +105,11 @@ func (tx *AccessListTx) gasFeeCap() *big.Int { return tx.GasPrice } func (tx *AccessListTx) value() *big.Int { return tx.Value } func (tx *AccessListTx) nonce() uint64 { return tx.Nonce } func (tx *AccessListTx) to() *common.Address { return tx.To } +func (tx *AccessListTx) expiredTime() uint64 { return 0 } + +func (tx *AccessListTx) rawPayerSignatureValues() (v, r, s *big.Int) { + return nil, nil, nil +} func (tx *AccessListTx) rawSignatureValues() (v, r, s *big.Int) { return tx.V, tx.R, tx.S diff --git a/core/types/dynamic_fee_tx.go b/core/types/dynamic_fee_tx.go index 585c029d89..ffab45df66 100644 --- a/core/types/dynamic_fee_tx.go +++ b/core/types/dynamic_fee_tx.go @@ -93,6 +93,11 @@ func (tx *DynamicFeeTx) gasPrice() *big.Int { return tx.GasFeeCap } func (tx *DynamicFeeTx) value() *big.Int { return tx.Value } func (tx *DynamicFeeTx) nonce() uint64 { return tx.Nonce } func (tx *DynamicFeeTx) to() *common.Address { return tx.To } +func (tx *DynamicFeeTx) expiredTime() uint64 { return 0 } + +func (tx *DynamicFeeTx) rawPayerSignatureValues() (v, r, s *big.Int) { + return nil, nil, nil +} func (tx *DynamicFeeTx) rawSignatureValues() (v, r, s *big.Int) { return tx.V, tx.R, tx.S diff --git a/core/types/legacy_tx.go b/core/types/legacy_tx.go index cb86bed772..3c2d007be6 100644 --- a/core/types/legacy_tx.go +++ b/core/types/legacy_tx.go @@ -102,6 +102,11 @@ func (tx *LegacyTx) gasFeeCap() *big.Int { return tx.GasPrice } func (tx *LegacyTx) value() *big.Int { return tx.Value } func (tx *LegacyTx) nonce() uint64 { return tx.Nonce } func (tx *LegacyTx) to() *common.Address { return tx.To } +func (tx *LegacyTx) expiredTime() uint64 { return 0 } + +func (tx *LegacyTx) rawPayerSignatureValues() (v, r, s *big.Int) { + return nil, nil, nil +} func (tx *LegacyTx) rawSignatureValues() (v, r, s *big.Int) { return tx.V, tx.R, tx.S diff --git a/core/types/receipt.go b/core/types/receipt.go index c3588990c0..264ccb4515 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -192,7 +192,7 @@ func (r *Receipt) DecodeRLP(s *rlp.Stream) error { return errEmptyTypedReceipt } r.Type = b[0] - if r.Type == AccessListTxType || r.Type == DynamicFeeTxType { + if r.Type == AccessListTxType || r.Type == DynamicFeeTxType || r.Type == SponsoredTxType { var dec receiptRLP if err := rlp.DecodeBytes(b[1:], &dec); err != nil { return err @@ -228,7 +228,7 @@ func (r *Receipt) decodeTyped(b []byte) error { return errEmptyTypedReceipt } switch b[0] { - case DynamicFeeTxType, AccessListTxType: + case DynamicFeeTxType, AccessListTxType, SponsoredTxType: var data receiptRLP err := rlp.DecodeBytes(b[1:], &data) if err != nil { @@ -391,11 +391,8 @@ func (rs Receipts) EncodeIndex(i int, w *bytes.Buffer) { switch r.Type { case LegacyTxType: rlp.Encode(w, data) - case AccessListTxType: - w.WriteByte(AccessListTxType) - rlp.Encode(w, data) - case DynamicFeeTxType: - w.WriteByte(DynamicFeeTxType) + case AccessListTxType, DynamicFeeTxType, SponsoredTxType: + w.WriteByte(r.Type) rlp.Encode(w, data) default: // For unsupported types, write nothing. Since this is for diff --git a/core/types/receipt_test.go b/core/types/receipt_test.go index 613559a658..ee9308395e 100644 --- a/core/types/receipt_test.go +++ b/core/types/receipt_test.go @@ -80,6 +80,23 @@ var ( }, Type: DynamicFeeTxType, } + mikoReceipt = &Receipt{ + Status: ReceiptStatusFailed, + CumulativeGasUsed: 1, + Logs: []*Log{ + { + Address: common.BytesToAddress([]byte{0x11}), + Topics: []common.Hash{common.HexToHash("dead"), common.HexToHash("beef")}, + Data: []byte{0x01, 0x00, 0xff}, + }, + { + Address: common.BytesToAddress([]byte{0x01, 0x11}), + Topics: []common.Hash{common.HexToHash("dead"), common.HexToHash("beef")}, + Data: []byte{0x01, 0x00, 0xff}, + }, + }, + Type: SponsoredTxType, + } ) func TestDecodeEmptyTypedReceipt(t *testing.T) { @@ -427,6 +444,25 @@ func TestReceiptMarshalBinary(t *testing.T) { if !bytes.Equal(have, eip1559Want) { t.Errorf("encoded RLP mismatch, got %x want %x", have, eip1559Want) } + + // Miko Receipt + buf.Reset() + mikoReceipt.Bloom = CreateBloom(Receipts{mikoReceipt}) + have, err = mikoReceipt.MarshalBinary() + if err != nil { + t.Fatalf("marshal binary error: %v", err) + } + mikoReceipts := Receipts{mikoReceipt} + mikoReceipts.EncodeIndex(0, buf) + haveEncodeIndex = buf.Bytes() + if !bytes.Equal(have, haveEncodeIndex) { + t.Errorf("BinaryMarshal and EncodeIndex mismatch, got %x want %x", have, haveEncodeIndex) + } + mikoWant := common.FromHex("64f901c58001b9010000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000010000080000000000000000000004000000000000000000000000000040000000000000000000000000000800000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000f8bef85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100fff85d940000000000000000000000000000000000000111f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff") + if !bytes.Equal(have, mikoWant) { + t.Errorf("encoded RLP mismatch, got %x want %x", have, mikoWant) + } + } func TestReceiptUnmarshalBinary(t *testing.T) { @@ -462,6 +498,17 @@ func TestReceiptUnmarshalBinary(t *testing.T) { if !reflect.DeepEqual(got1559Receipt, eip1559Receipt) { t.Errorf("receipt unmarshalled from binary mismatch, got %v want %v", got1559Receipt, eip1559Receipt) } + + // miko Receipt + mikoRctBinary := common.FromHex("64f901c58001b9010000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000010000080000000000000000000004000000000000000000000000000040000000000000000000000000000800000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000f8bef85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100fff85d940000000000000000000000000000000000000111f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff") + gotMikoReceipt := new(Receipt) + if err := gotMikoReceipt.UnmarshalBinary(mikoRctBinary); err != nil { + t.Fatalf("unmarshal binary error: %v", err) + } + mikoReceipt.Bloom = CreateBloom(Receipts{mikoReceipt}) + if !reflect.DeepEqual(gotMikoReceipt, mikoReceipt) { + t.Errorf("receipt unmarshalled from binary mismatch, got %v want %v", gotMikoReceipt, mikoReceipt) + } } func clearComputedFieldsOnReceipts(t *testing.T, receipts Receipts) { diff --git a/core/types/sponsored_tx.go b/core/types/sponsored_tx.go new file mode 100644 index 0000000000..3b9074e407 --- /dev/null +++ b/core/types/sponsored_tx.go @@ -0,0 +1,99 @@ +package types + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +type SponsoredTx struct { + ChainID *big.Int // destination chain ID + Nonce uint64 // nonce of sender account + GasTipCap *big.Int // maximum tip to the miner + GasFeeCap *big.Int // maximum gas fee want to pay + Gas uint64 // gas limit + To *common.Address `rlp:"nil"` // nil means contract creation + Value *big.Int // wei amount + Data []byte // contract invocation input data + ExpiredTime uint64 // the expired time of payer's signature + PayerV, PayerR, PayerS *big.Int // payer's signature values + V, R, S *big.Int // sender's signature values +} + +func (tx *SponsoredTx) copy() TxData { + cpy := &SponsoredTx{ + Nonce: tx.Nonce, + To: copyAddressPtr(tx.To), + Data: common.CopyBytes(tx.Data), + Gas: tx.Gas, + ExpiredTime: tx.ExpiredTime, + // These are initialized below. + ChainID: new(big.Int), + Value: new(big.Int), + GasTipCap: new(big.Int), + GasFeeCap: new(big.Int), + PayerV: new(big.Int), + PayerR: new(big.Int), + PayerS: new(big.Int), + V: new(big.Int), + R: new(big.Int), + S: new(big.Int), + } + if tx.ChainID != nil { + cpy.ChainID.Set(tx.ChainID) + } + if tx.Value != nil { + cpy.Value.Set(tx.Value) + } + if tx.GasTipCap != nil { + cpy.GasTipCap.Set(tx.GasTipCap) + } + if tx.GasFeeCap != nil { + cpy.GasFeeCap.Set(tx.GasFeeCap) + } + if tx.PayerV != nil { + cpy.PayerV.Set(tx.PayerV) + } + if tx.PayerR != nil { + cpy.PayerR.Set(tx.PayerR) + } + if tx.PayerS != nil { + cpy.PayerS.Set(tx.PayerS) + } + if tx.V != nil { + cpy.V.Set(tx.V) + } + if tx.R != nil { + cpy.R.Set(tx.R) + } + if tx.S != nil { + cpy.S.Set(tx.S) + } + return cpy +} + +// accessors for innerTx. +func (tx *SponsoredTx) txType() byte { return SponsoredTxType } +func (tx *SponsoredTx) chainID() *big.Int { return tx.ChainID } +func (tx *SponsoredTx) accessList() AccessList { return nil } +func (tx *SponsoredTx) data() []byte { return tx.Data } +func (tx *SponsoredTx) gas() uint64 { return tx.Gas } +func (tx *SponsoredTx) gasPrice() *big.Int { return tx.GasFeeCap } +func (tx *SponsoredTx) gasTipCap() *big.Int { return tx.GasTipCap } +func (tx *SponsoredTx) gasFeeCap() *big.Int { return tx.GasFeeCap } +func (tx *SponsoredTx) value() *big.Int { return tx.Value } +func (tx *SponsoredTx) nonce() uint64 { return tx.Nonce } +func (tx *SponsoredTx) to() *common.Address { return tx.To } +func (tx *SponsoredTx) expiredTime() uint64 { return tx.ExpiredTime } + +func (tx *SponsoredTx) rawPayerSignatureValues() (v, r, s *big.Int) { + return tx.PayerV, tx.PayerR, tx.PayerS +} + +func (tx *SponsoredTx) rawSignatureValues() (v, r, s *big.Int) { + return tx.V, tx.R, tx.S +} + +func (tx *SponsoredTx) setSignatureValues(chainID, v, r, s *big.Int) { + tx.ChainID, tx.V, tx.R, tx.S = chainID, v, r, s +} diff --git a/core/types/transaction.go b/core/types/transaction.go index 4e3c4fad8a..f19850c04e 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -32,12 +32,13 @@ import ( ) var ( - ErrInvalidSig = errors.New("invalid transaction v, r, s values") - ErrUnexpectedProtection = errors.New("transaction type does not supported EIP-155 protected signatures") - ErrInvalidTxType = errors.New("transaction type not valid in this context") - ErrTxTypeNotSupported = errors.New("transaction type not supported") - ErrGasFeeCapTooLow = errors.New("fee cap less than base fee") - errEmptyTypedTx = errors.New("empty typed transaction bytes") + ErrInvalidSig = errors.New("invalid transaction v, r, s values") + ErrUnexpectedProtection = errors.New("transaction type does not supported EIP-155 protected signatures") + ErrInvalidTxType = errors.New("transaction type not valid in this context") + ErrTxTypeNotSupported = errors.New("transaction type not supported") + ErrGasFeeCapTooLow = errors.New("fee cap less than base fee") + ErrSamePayerSenderSponsoredTx = errors.New("payer = sender in sponsored transaction") + errEmptyTypedTx = errors.New("empty typed transaction bytes") ) // Transaction types. @@ -45,6 +46,7 @@ const ( LegacyTxType = iota AccessListTxType DynamicFeeTxType + SponsoredTxType = 100 ) // Transaction is an Ethereum transaction. @@ -53,9 +55,10 @@ type Transaction struct { time time.Time // Time first seen locally (spam avoidance) // caches - hash atomic.Value - size atomic.Value - from atomic.Value + hash atomic.Value + size atomic.Value + from atomic.Value + payer atomic.Value } // NewTx creates a new transaction. @@ -67,7 +70,7 @@ func NewTx(inner TxData) *Transaction { // TxData is the underlying data of a transaction. // -// This is implemented by DynamicFeeTx, LegacyTx and AccessListTx. +// This is implemented by DynamicFeeTx, LegacyTx, SponsoredTx and AccessListTx. type TxData interface { txType() byte // returns the type ID copy() TxData // creates a deep copy and initializes all fields @@ -82,7 +85,9 @@ type TxData interface { value() *big.Int nonce() uint64 to() *common.Address + expiredTime() uint64 + rawPayerSignatureValues() (v, r, s *big.Int) rawSignatureValues() (v, r, s *big.Int) setSignatureValues(chainID, v, r, s *big.Int) } @@ -186,6 +191,10 @@ func (tx *Transaction) decodeTyped(b []byte) (TxData, error) { var inner DynamicFeeTx err := rlp.DecodeBytes(b[1:], &inner) return &inner, err + case SponsoredTxType: + var inner SponsoredTx + err := rlp.DecodeBytes(b[1:], &inner) + return &inner, err default: return nil, ErrTxTypeNotSupported } @@ -287,6 +296,11 @@ func (tx *Transaction) To() *common.Address { return copyAddressPtr(tx.inner.to()) } +// ExpiredTime returns the expired time of the sponsored transaction +func (tx *Transaction) ExpiredTime() uint64 { + return tx.inner.expiredTime() +} + // Cost returns gas * gasPrice + value. func (tx *Transaction) Cost() *big.Int { total := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) @@ -294,6 +308,12 @@ func (tx *Transaction) Cost() *big.Int { return total } +// RawSignatureValues returns the V, R, S payer signature values of the transaction. +// The return values should not be modified by the caller. +func (tx *Transaction) RawPayerSignatureValues() (v, r, s *big.Int) { + return tx.inner.rawPayerSignatureValues() +} + // RawSignatureValues returns the V, R, S signature values of the transaction. // The return values should not be modified by the caller. func (tx *Transaction) RawSignatureValues() (v, r, s *big.Int) { @@ -567,48 +587,64 @@ func (t *TransactionsByPriceAndNonce) Size() int { // // NOTE: In a future PR this will be removed. type Message struct { - to *common.Address - from common.Address - nonce uint64 - amount *big.Int - gasLimit uint64 - gasPrice *big.Int - gasFeeCap *big.Int - gasTipCap *big.Int - data []byte - accessList AccessList - isFake bool -} - -func NewMessage(from common.Address, to *common.Address, nonce uint64, amount *big.Int, gasLimit uint64, gasPrice, gasFeeCap, gasTipCap *big.Int, data []byte, accessList AccessList, isFake bool) Message { + to *common.Address + from common.Address + nonce uint64 + amount *big.Int + gasLimit uint64 + gasPrice *big.Int + gasFeeCap *big.Int + gasTipCap *big.Int + data []byte + accessList AccessList + isFake bool + payer common.Address + expiredTime uint64 +} + +// Create a new message with payer is the same as from, expired time = 0 +func NewMessage( + from common.Address, + to *common.Address, + nonce uint64, + amount *big.Int, + gasLimit uint64, + gasPrice, gasFeeCap, gasTipCap *big.Int, + data []byte, + accessList AccessList, + isFake bool, +) Message { return Message{ - from: from, - to: to, - nonce: nonce, - amount: amount, - gasLimit: gasLimit, - gasPrice: gasPrice, - gasFeeCap: gasFeeCap, - gasTipCap: gasTipCap, - data: data, - accessList: accessList, - isFake: isFake, + from: from, + to: to, + nonce: nonce, + amount: amount, + gasLimit: gasLimit, + gasPrice: gasPrice, + gasFeeCap: gasFeeCap, + gasTipCap: gasTipCap, + data: data, + accessList: accessList, + isFake: isFake, + payer: from, + expiredTime: 0, } } // AsMessage returns the transaction as a core.Message. func (tx *Transaction) AsMessage(s Signer, baseFee *big.Int) (Message, error) { msg := Message{ - nonce: tx.Nonce(), - gasLimit: tx.Gas(), - gasPrice: new(big.Int).Set(tx.GasPrice()), - gasFeeCap: new(big.Int).Set(tx.GasFeeCap()), - gasTipCap: new(big.Int).Set(tx.GasTipCap()), - to: tx.To(), - amount: tx.Value(), - data: tx.Data(), - accessList: tx.AccessList(), - isFake: false, + nonce: tx.Nonce(), + gasLimit: tx.Gas(), + gasPrice: new(big.Int).Set(tx.GasPrice()), + gasFeeCap: new(big.Int).Set(tx.GasFeeCap()), + gasTipCap: new(big.Int).Set(tx.GasTipCap()), + to: tx.To(), + amount: tx.Value(), + data: tx.Data(), + accessList: tx.AccessList(), + isFake: false, + expiredTime: tx.ExpiredTime(), } // If baseFee provided, set gasPrice to effectiveGasPrice. if baseFee != nil { @@ -616,7 +652,25 @@ func (tx *Transaction) AsMessage(s Signer, baseFee *big.Int) (Message, error) { } var err error msg.from, err = Sender(s, tx) - return msg, err + if err != nil { + return Message{}, err + } + + if tx.Type() == SponsoredTxType { + msg.payer, err = Payer(s, tx) + if err != nil { + return Message{}, err + } + + if msg.payer == msg.from { + // Reject sponsored transaction with identical payer and sender + return Message{}, ErrSamePayerSenderSponsoredTx + } + return msg, nil + } else { + msg.payer = msg.from + return msg, nil + } } func (m Message) From() common.Address { return m.from } @@ -630,6 +684,8 @@ func (m Message) Nonce() uint64 { return m.nonce } func (m Message) Data() []byte { return m.data } func (m Message) AccessList() AccessList { return m.accessList } func (m Message) IsFake() bool { return m.isFake } +func (m Message) Payer() common.Address { return m.payer } +func (m Message) ExpiredTime() uint64 { return m.expiredTime } // copyAddressPtr copies an address. func copyAddressPtr(a *common.Address) *common.Address { diff --git a/core/types/transaction_marshalling.go b/core/types/transaction_marshalling.go index aad31a5a97..678c36fa3e 100644 --- a/core/types/transaction_marshalling.go +++ b/core/types/transaction_marshalling.go @@ -46,6 +46,12 @@ type txJSON struct { ChainID *hexutil.Big `json:"chainId,omitempty"` AccessList *AccessList `json:"accessList,omitempty"` + // Sponsored transaction fields + ExpiredTime *hexutil.Uint64 `json:"expiredTime,omitempty"` + PayerV *hexutil.Big `json:"payerV,omitempty"` + PayerR *hexutil.Big `json:"payerR,omitempty"` + PayerS *hexutil.Big `json:"payerS,omitempty"` + // Only used for encoding: Hash common.Hash `json:"hash"` } @@ -56,44 +62,40 @@ func (t *Transaction) MarshalJSON() ([]byte, error) { // These are set for all tx types. enc.Hash = t.Hash() enc.Type = hexutil.Uint64(t.Type()) + nonce := t.Nonce() + gas := t.Gas() + data := t.Data() + v, r, s := t.RawSignatureValues() + enc.Nonce = (*hexutil.Uint64)(&nonce) + enc.Gas = (*hexutil.Uint64)(&gas) + enc.Value = (*hexutil.Big)(t.Value()) + enc.Data = (*hexutil.Bytes)(&data) + enc.To = t.To() + enc.V = (*hexutil.Big)(v) + enc.R = (*hexutil.Big)(r) + enc.S = (*hexutil.Big)(s) // Other fields are set conditionally depending on tx type. switch tx := t.inner.(type) { case *LegacyTx: - enc.Nonce = (*hexutil.Uint64)(&tx.Nonce) - enc.Gas = (*hexutil.Uint64)(&tx.Gas) enc.GasPrice = (*hexutil.Big)(tx.GasPrice) - enc.Value = (*hexutil.Big)(tx.Value) - enc.Data = (*hexutil.Bytes)(&tx.Data) - enc.To = t.To() - enc.V = (*hexutil.Big)(tx.V) - enc.R = (*hexutil.Big)(tx.R) - enc.S = (*hexutil.Big)(tx.S) case *AccessListTx: + enc.GasPrice = (*hexutil.Big)(tx.GasPrice) enc.ChainID = (*hexutil.Big)(tx.ChainID) enc.AccessList = &tx.AccessList - enc.Nonce = (*hexutil.Uint64)(&tx.Nonce) - enc.Gas = (*hexutil.Uint64)(&tx.Gas) - enc.GasPrice = (*hexutil.Big)(tx.GasPrice) - enc.Value = (*hexutil.Big)(tx.Value) - enc.Data = (*hexutil.Bytes)(&tx.Data) - enc.To = t.To() - enc.V = (*hexutil.Big)(tx.V) - enc.R = (*hexutil.Big)(tx.R) - enc.S = (*hexutil.Big)(tx.S) case *DynamicFeeTx: enc.ChainID = (*hexutil.Big)(tx.ChainID) enc.AccessList = &tx.AccessList - enc.Nonce = (*hexutil.Uint64)(&tx.Nonce) - enc.Gas = (*hexutil.Uint64)(&tx.Gas) enc.MaxFeePerGas = (*hexutil.Big)(tx.GasFeeCap) enc.MaxPriorityFeePerGas = (*hexutil.Big)(tx.GasTipCap) - enc.Value = (*hexutil.Big)(tx.Value) - enc.Data = (*hexutil.Bytes)(&tx.Data) - enc.To = t.To() - enc.V = (*hexutil.Big)(tx.V) - enc.R = (*hexutil.Big)(tx.R) - enc.S = (*hexutil.Big)(tx.S) + case *SponsoredTx: + enc.ChainID = (*hexutil.Big)(tx.ChainID) + enc.MaxFeePerGas = (*hexutil.Big)(tx.GasFeeCap) + enc.MaxPriorityFeePerGas = (*hexutil.Big)(tx.GasTipCap) + enc.ExpiredTime = (*hexutil.Uint64)(&tx.ExpiredTime) + enc.PayerV = (*hexutil.Big)(tx.PayerV) + enc.PayerR = (*hexutil.Big)(tx.PayerR) + enc.PayerS = (*hexutil.Big)(tx.PayerS) } return json.Marshal(&enc) } @@ -105,56 +107,81 @@ func (t *Transaction) UnmarshalJSON(input []byte) error { return err } + var to *common.Address + if dec.To != nil { + to = dec.To + } + if dec.Nonce == nil { + return errors.New("missing required field 'nonce' in transaction") + } + nonce := uint64(*dec.Nonce) + if dec.Gas == nil { + return errors.New("missing required field 'gas' in transaction") + } + gas := uint64(*dec.Gas) + if dec.Value == nil { + return errors.New("missing required field 'value' in transaction") + } + value := (*big.Int)(dec.Value) + if dec.Data == nil { + return errors.New("missing required field 'input' in transaction") + } + data := *dec.Data + if dec.V == nil { + return errors.New("missing required field 'v' in transaction") + } + v := (*big.Int)(dec.V) + if dec.R == nil { + return errors.New("missing required field 'r' in transaction") + } + r := (*big.Int)(dec.R) + if dec.S == nil { + return errors.New("missing required field 's' in transaction") + } + s := (*big.Int)(dec.S) + withSignature := v.Sign() != 0 || r.Sign() != 0 || s.Sign() != 0 + if withSignature { + maybeProtected := false + if dec.Type == LegacyTxType { + maybeProtected = true + } + + if err := sanityCheckSignature(v, r, s, maybeProtected); err != nil { + return err + } + } + // Decode / verify fields according to transaction type. var inner TxData switch dec.Type { case LegacyTxType: - var itx LegacyTx - inner = &itx - if dec.To != nil { - itx.To = dec.To - } - if dec.Nonce == nil { - return errors.New("missing required field 'nonce' in transaction") + itx := LegacyTx{ + Nonce: nonce, + Gas: gas, + To: to, + Value: value, + Data: data, + V: v, + R: r, + S: s, } - itx.Nonce = uint64(*dec.Nonce) + inner = &itx if dec.GasPrice == nil { return errors.New("missing required field 'gasPrice' in transaction") } itx.GasPrice = (*big.Int)(dec.GasPrice) - if dec.Gas == nil { - return errors.New("missing required field 'gas' in transaction") - } - itx.Gas = uint64(*dec.Gas) - if dec.Value == nil { - return errors.New("missing required field 'value' in transaction") - } - itx.Value = (*big.Int)(dec.Value) - if dec.Data == nil { - return errors.New("missing required field 'input' in transaction") - } - itx.Data = *dec.Data - if dec.V == nil { - return errors.New("missing required field 'v' in transaction") - } - itx.V = (*big.Int)(dec.V) - if dec.R == nil { - return errors.New("missing required field 'r' in transaction") - } - itx.R = (*big.Int)(dec.R) - if dec.S == nil { - return errors.New("missing required field 's' in transaction") - } - itx.S = (*big.Int)(dec.S) - withSignature := itx.V.Sign() != 0 || itx.R.Sign() != 0 || itx.S.Sign() != 0 - if withSignature { - if err := sanityCheckSignature(itx.V, itx.R, itx.S, true); err != nil { - return err - } - } case AccessListTxType: - var itx AccessListTx + itx := AccessListTx{ + Nonce: nonce, + Gas: gas, + To: to, + Value: value, + Data: data, + V: v, + R: r, + S: s, + } inner = &itx // Access list is optional for now. if dec.AccessList != nil { @@ -164,50 +191,22 @@ func (t *Transaction) UnmarshalJSON(input []byte) error { return errors.New("missing required field 'chainId' in transaction") } itx.ChainID = (*big.Int)(dec.ChainID) - if dec.To != nil { - itx.To = dec.To - } - if dec.Nonce == nil { - return errors.New("missing required field 'nonce' in transaction") - } - itx.Nonce = uint64(*dec.Nonce) if dec.GasPrice == nil { return errors.New("missing required field 'gasPrice' in transaction") } itx.GasPrice = (*big.Int)(dec.GasPrice) - if dec.Gas == nil { - return errors.New("missing required field 'gas' in transaction") - } - itx.Gas = uint64(*dec.Gas) - if dec.Value == nil { - return errors.New("missing required field 'value' in transaction") - } - itx.Value = (*big.Int)(dec.Value) - if dec.Data == nil { - return errors.New("missing required field 'input' in transaction") - } - itx.Data = *dec.Data - if dec.V == nil { - return errors.New("missing required field 'v' in transaction") - } - itx.V = (*big.Int)(dec.V) - if dec.R == nil { - return errors.New("missing required field 'r' in transaction") - } - itx.R = (*big.Int)(dec.R) - if dec.S == nil { - return errors.New("missing required field 's' in transaction") - } - itx.S = (*big.Int)(dec.S) - withSignature := itx.V.Sign() != 0 || itx.R.Sign() != 0 || itx.S.Sign() != 0 - if withSignature { - if err := sanityCheckSignature(itx.V, itx.R, itx.S, false); err != nil { - return err - } - } case DynamicFeeTxType: - var itx DynamicFeeTx + itx := DynamicFeeTx{ + Nonce: nonce, + Gas: gas, + To: to, + Value: value, + Data: data, + V: v, + R: r, + S: s, + } inner = &itx // Access list is optional for now. if dec.AccessList != nil { @@ -217,13 +216,6 @@ func (t *Transaction) UnmarshalJSON(input []byte) error { return errors.New("missing required field 'chainId' in transaction") } itx.ChainID = (*big.Int)(dec.ChainID) - if dec.To != nil { - itx.To = dec.To - } - if dec.Nonce == nil { - return errors.New("missing required field 'nonce' in transaction") - } - itx.Nonce = uint64(*dec.Nonce) if dec.MaxPriorityFeePerGas == nil { return errors.New("missing required field 'maxPriorityFeePerGas' for txdata") } @@ -232,35 +224,49 @@ func (t *Transaction) UnmarshalJSON(input []byte) error { return errors.New("missing required field 'maxFeePerGas' for txdata") } itx.GasFeeCap = (*big.Int)(dec.MaxFeePerGas) - if dec.Gas == nil { - return errors.New("missing required field 'gas' for txdata") + + case SponsoredTxType: + itx := SponsoredTx{ + Nonce: nonce, + Gas: gas, + To: to, + Value: value, + Data: data, + V: v, + R: r, + S: s, } - itx.Gas = uint64(*dec.Gas) - if dec.Value == nil { - return errors.New("missing required field 'value' in transaction") + inner = &itx + if dec.ChainID == nil { + return errors.New("missing required field 'chainId' in transaction") } - itx.Value = (*big.Int)(dec.Value) - if dec.Data == nil { - return errors.New("missing required field 'input' in transaction") + itx.ChainID = (*big.Int)(dec.ChainID) + if dec.MaxPriorityFeePerGas == nil { + return errors.New("missing required field 'maxPriorityFeePerGas' for txdata") + } + itx.GasTipCap = (*big.Int)(dec.MaxPriorityFeePerGas) + if dec.MaxFeePerGas == nil { + return errors.New("missing required field 'maxFeePerGas' for txdata") + } + itx.GasFeeCap = (*big.Int)(dec.MaxFeePerGas) + if dec.ExpiredTime == nil { + return errors.New("missing required field 'expiredTime' in transaction") } - itx.Data = *dec.Data - if dec.V == nil { - return errors.New("missing required field 'v' in transaction") + itx.ExpiredTime = uint64(*dec.ExpiredTime) + if dec.PayerV == nil { + return errors.New("missing required field 'payerV' in transaction") } - itx.V = (*big.Int)(dec.V) - if dec.R == nil { - return errors.New("missing required field 'r' in transaction") + itx.PayerV = (*big.Int)(dec.PayerV) + if dec.PayerR == nil { + return errors.New("missing required field 'payerR' in transaction") } - itx.R = (*big.Int)(dec.R) - if dec.S == nil { - return errors.New("missing required field 's' in transaction") + itx.PayerR = (*big.Int)(dec.PayerR) + if dec.PayerS == nil { + return errors.New("missing required field 'payerS' in transaction") } - itx.S = (*big.Int)(dec.S) - withSignature := itx.V.Sign() != 0 || itx.R.Sign() != 0 || itx.S.Sign() != 0 - if withSignature { - if err := sanityCheckSignature(itx.V, itx.R, itx.S, false); err != nil { - return err - } + itx.PayerS = (*big.Int)(dec.PayerS) + if err := sanityCheckSignature(itx.PayerV, itx.PayerR, itx.PayerS, false); err != nil { + return err } default: diff --git a/core/types/transaction_signing.go b/core/types/transaction_signing.go index 4b5dd16aa2..8fd5f0f5cf 100644 --- a/core/types/transaction_signing.go +++ b/core/types/transaction_signing.go @@ -27,7 +27,10 @@ import ( "github.com/ethereum/go-ethereum/params" ) -var ErrInvalidChainId = errors.New("invalid chain id for signer") +var ( + ErrInvalidChainId = errors.New("invalid chain id for signer") + errMissingPayerField = errors.New("transaction has no payer field") +) // sigCache is used to cache the derived sender and contains // the signer used to derive it. @@ -44,6 +47,8 @@ func MakeSigner(config *params.ChainConfig, blockNumber *big.Int) Signer { signer = NewLondonSigner(config.GetChainID(blockNumber)) case config.IsBerlin(blockNumber): signer = NewEIP2930Signer(config.GetChainID(blockNumber)) + case config.IsMiko(blockNumber): + signer = NewMikoSigner(config.GetChainID(blockNumber)) case config.IsEIP155(blockNumber): signer = NewEIP155Signer(config.GetChainID(blockNumber)) case config.IsHomestead(blockNumber): @@ -69,6 +74,9 @@ func LatestSigner(config *params.ChainConfig) Signer { if config.BerlinBlock != nil { return NewEIP2930Signer(config.ChainID) } + if config.MikoBlock != nil { + return NewMikoSigner(config.ChainID) + } if config.EIP155Block != nil { return NewEIP155Signer(config.ChainID) } @@ -111,6 +119,30 @@ func SignNewTx(prv *ecdsa.PrivateKey, s Signer, txdata TxData) (*Transaction, er return tx.WithSignature(s, sig) } +func PayerSign(prv *ecdsa.PrivateKey, signer Signer, sender common.Address, txdata TxData) (r, s, v *big.Int, err error) { + payerHash := rlpHash([]interface{}{ + signer.ChainID(), + sender, + txdata.nonce(), + txdata.gasTipCap(), + txdata.gasFeeCap(), + txdata.gas(), + txdata.to(), + txdata.value(), + txdata.data(), + txdata.expiredTime(), + }) + + sig, err := crypto.Sign(payerHash[:], prv) + if err != nil { + return nil, nil, nil, err + } + + r, s, _ = decodeSignature(sig) + v = big.NewInt(int64(sig[64])) + return r, s, v, nil +} + // MustSignNewTx creates a transaction and signs it. // This panics if the transaction cannot be signed. func MustSignNewTx(prv *ecdsa.PrivateKey, s Signer, txdata TxData) *Transaction { @@ -147,6 +179,35 @@ func Sender(signer Signer, tx *Transaction) (common.Address, error) { return addr, nil } +// Payer returns the address derived from payer's signature in sponsored +// transaction or nil in other transaction types. +// +// Payer may cache the address, allowing it to be used regardless of +// signing method. The cache is invalidated if the cached signer does +// not match the signer used in the current call. +func Payer(signer Signer, tx *Transaction) (common.Address, error) { + if tx.Type() != SponsoredTxType { + return common.Address{}, errMissingPayerField + } + + if sc := tx.payer.Load(); sc != nil { + sigCache := sc.(sigCache) + // If the signer used to derive from in a previous + // call is not the same as used current, invalidate + // the cache. + if sigCache.signer.Equal(signer) { + return sigCache.from, nil + } + } + + addr, err := signer.Payer(tx) + if err != nil { + return common.Address{}, err + } + tx.payer.Store(sigCache{signer: signer, from: addr}) + return addr, nil +} + // Signer encapsulates transaction signature handling. The name of this type is slightly // misleading because Signers don't actually sign, they're just for validating and // processing of signatures. @@ -168,6 +229,9 @@ type Signer interface { // Equal returns true if the given signer is the same as the receiver. Equal(Signer) bool + + // Payer returns the payer address of sponsored transaction + Payer(tx *Transaction) (common.Address, error) } type londonSigner struct{ eip2930Signer } @@ -175,10 +239,11 @@ type londonSigner struct{ eip2930Signer } // NewLondonSigner returns a signer that accepts // - EIP-1559 dynamic fee transactions // - EIP-2930 access list transactions, +// - Sponsored transactions, // - EIP-155 replay protected transactions, and // - legacy Homestead transactions. func NewLondonSigner(chainId *big.Int) Signer { - return londonSigner{eip2930Signer{NewEIP155Signer(chainId)}} + return londonSigner{eip2930Signer{MikoSigner{NewEIP155Signer(chainId)}}} } func (s londonSigner) Sender(tx *Transaction) (common.Address, error) { @@ -236,12 +301,16 @@ func (s londonSigner) Hash(tx *Transaction) common.Hash { }) } -type eip2930Signer struct{ EIP155Signer } +func (s londonSigner) Payer(tx *Transaction) (common.Address, error) { + return payerInternal(s, tx) +} + +type eip2930Signer struct{ MikoSigner } // NewEIP2930Signer returns a signer that accepts EIP-2930 access list transactions, // EIP-155 replay protected transactions, and legacy Homestead transactions. func NewEIP2930Signer(chainId *big.Int) Signer { - return eip2930Signer{NewEIP155Signer(chainId)} + return eip2930Signer{MikoSigner{NewEIP155Signer(chainId)}} } func (s eip2930Signer) ChainID() *big.Int { @@ -256,12 +325,8 @@ func (s eip2930Signer) Equal(s2 Signer) bool { func (s eip2930Signer) Sender(tx *Transaction) (common.Address, error) { V, R, S := tx.RawSignatureValues() switch tx.Type() { - case LegacyTxType: - if !tx.Protected() { - return HomesteadSigner{}.Sender(tx) - } - V = new(big.Int).Sub(V, s.chainIdMul) - V.Sub(V, big8) + case LegacyTxType, SponsoredTxType: + return s.MikoSigner.Sender(tx) case AccessListTxType: // AL txs are defined to use 0 and 1 as their recovery // id, add 27 to become equivalent to unprotected Homestead signatures. @@ -277,8 +342,8 @@ func (s eip2930Signer) Sender(tx *Transaction) (common.Address, error) { func (s eip2930Signer) SignatureValues(tx *Transaction, sig []byte) (R, S, V *big.Int, err error) { switch txdata := tx.inner.(type) { - case *LegacyTx: - return s.EIP155Signer.SignatureValues(tx, sig) + case *LegacyTx, *SponsoredTx: + return s.MikoSigner.SignatureValues(tx, sig) case *AccessListTx: // Check that chain ID of tx matches the signer. We also accept ID zero here, // because it indicates that the chain ID was not specified in the tx. @@ -320,6 +385,8 @@ func (s eip2930Signer) Hash(tx *Transaction) common.Hash { tx.Data(), tx.AccessList(), }) + case SponsoredTxType: + return s.MikoSigner.Hash(tx) default: // This _should_ not happen, but in case someone sends in a bad // json struct via RPC, it's probably more prudent to return an @@ -329,6 +396,124 @@ func (s eip2930Signer) Hash(tx *Transaction) common.Hash { } } +// eip2930Signer and londonSigner must define Payer instead of using +// the same Payer defined in base MikoSigner because internally, Payer +// calls to Sender(signer, tx) which supports caching the recovered +// sender if the signer matches. So if we let the MikoSigner.Payer be +// called, the signer passed to Sender(signer, tx) is MikoSigner not +// eip2930Signer/londonSigner which unmatches with signer in cached +// recovered address. As a result, the sender must be recovered again +// which wastes CPU. +func (s eip2930Signer) Payer(tx *Transaction) (common.Address, error) { + return payerInternal(s, tx) +} + +type MikoSigner struct { + EIP155Signer +} + +func NewMikoSigner(chainId *big.Int) MikoSigner { + return MikoSigner{NewEIP155Signer(chainId)} +} + +func (s MikoSigner) Equal(s2 Signer) bool { + miko, ok := s2.(MikoSigner) + return ok && miko.chainId.Cmp(s.chainId) == 0 +} + +func (s MikoSigner) Sender(tx *Transaction) (common.Address, error) { + switch tx.Type() { + case LegacyTxType: + return s.EIP155Signer.Sender(tx) + case SponsoredTxType: + if tx.ChainId().Cmp(s.chainId) != 0 { + return common.Address{}, ErrInvalidChainId + } + // V in sponsored signature is {0, 1}, but the recoverPlain expects + // {0, 1} + 27, so we need to add 27 to V + V, R, S := tx.RawSignatureValues() + V = new(big.Int).Add(V, big.NewInt(27)) + return recoverPlain(s.Hash(tx), R, S, V, true) + default: + return common.Address{}, ErrTxTypeNotSupported + } +} + +func (s MikoSigner) SignatureValues(tx *Transaction, sig []byte) (R, S, V *big.Int, err error) { + switch tx.Type() { + case LegacyTxType: + return s.EIP155Signer.SignatureValues(tx, sig) + case SponsoredTxType: + // V in sponsored signature is {0, 1}, get it directly from raw signature + // because decodeSignature returns {0, 1} + 27 + R, S, _ := decodeSignature(sig) + V := big.NewInt(int64(sig[64])) + return R, S, V, nil + default: + return nil, nil, nil, ErrTxTypeNotSupported + } +} + +func (s MikoSigner) Hash(tx *Transaction) common.Hash { + switch tx.Type() { + case LegacyTxType: + return s.EIP155Signer.Hash(tx) + case SponsoredTxType: + payerV, payerR, payerS := tx.RawPayerSignatureValues() + return prefixedRlpHash( + tx.Type(), + []interface{}{ + s.chainId, + tx.Nonce(), + tx.GasTipCap(), + tx.GasFeeCap(), + tx.Gas(), + tx.To(), + tx.Value(), + tx.Data(), + tx.ExpiredTime(), + payerV, payerR, payerS, + }, + ) + default: + return common.Hash{} + } +} + +func payerInternal(s Signer, tx *Transaction) (common.Address, error) { + if tx.Type() != SponsoredTxType { + return common.Address{}, ErrInvalidTxType + } + + sender, err := Sender(s, tx) + if err != nil { + return common.Address{}, err + } + + payerV, payerR, payerS := tx.RawPayerSignatureValues() + payerHash := rlpHash([]interface{}{ + tx.ChainId(), // The chainId is checked in Sender already + sender, + tx.Nonce(), + tx.GasTipCap(), + tx.GasFeeCap(), + tx.Gas(), + tx.To(), + tx.Value(), + tx.Data(), + tx.ExpiredTime(), + }) + + // V in payer signature is {0, 1}, but the recoverPlain expects + // {0, 1} + 27, so we need to add 27 to V + payerV = new(big.Int).Add(payerV, big.NewInt(27)) + return recoverPlain(payerHash, payerR, payerS, payerV, true) +} + +func (s MikoSigner) Payer(tx *Transaction) (common.Address, error) { + return payerInternal(s, tx) +} + // EIP155Signer implements Signer using the EIP-155 rules. This accepts transactions which // are replay-protected as well as unprotected homestead transactions. type EIP155Signer struct { @@ -400,6 +585,10 @@ func (s EIP155Signer) Hash(tx *Transaction) common.Hash { }) } +func (s EIP155Signer) Payer(tx *Transaction) (common.Address, error) { + return common.Address{}, ErrInvalidTxType +} + // HomesteadTransaction implements TransactionInterface using the // homestead rules. type HomesteadSigner struct{ FrontierSigner } @@ -469,6 +658,10 @@ func (fs FrontierSigner) Hash(tx *Transaction) common.Hash { }) } +func (fs FrontierSigner) Payer(tx *Transaction) (common.Address, error) { + return common.Address{}, ErrInvalidTxType +} + func decodeSignature(sig []byte) (r, s, v *big.Int) { if len(sig) != crypto.SignatureLength { panic(fmt.Sprintf("wrong size for signature: got %d, want %d", len(sig), crypto.SignatureLength)) diff --git a/core/types/transaction_signing_test.go b/core/types/transaction_signing_test.go index 689fc38a9b..4e2c634c66 100644 --- a/core/types/transaction_signing_test.go +++ b/core/types/transaction_signing_test.go @@ -17,6 +17,7 @@ package types import ( + "errors" "math/big" "testing" @@ -136,3 +137,164 @@ func TestChainId(t *testing.T) { t.Error("expected no error") } } + +func TestSponsoredTransactionSigner(t *testing.T) { + recipient := common.HexToAddress("0000000000000000000000000000000000000001") + sender, err := crypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + senderAddr := crypto.PubkeyToAddress(sender.PublicKey) + + payer, err := crypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + payerAddr := crypto.PubkeyToAddress(payer.PublicKey) + + mikoSigner := NewMikoSigner(big.NewInt(2020)) + eip155Signer := NewEIP155Signer(big.NewInt(2020)) + latestSigner := LatestSignerForChainID(big.NewInt(2020)) + + innerTx := 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, + } + + innerTx.PayerR, innerTx.PayerS, innerTx.PayerV, err = PayerSign(payer, mikoSigner, senderAddr, &innerTx) + if err != nil { + t.Fatalf("Payer fails to sign, err %s", err) + } + + // 1. Check hash + tx := NewTx(&innerTx) + + expectedHash := prefixedRlpHash(0x64, []interface{}{ + big.NewInt(2020), + innerTx.Nonce, + innerTx.GasTipCap, + innerTx.GasFeeCap, + innerTx.Gas, + innerTx.To, + innerTx.Value, + innerTx.Data, + innerTx.ExpiredTime, + innerTx.PayerV, + innerTx.PayerR, + innerTx.PayerS, + }) + + hash := mikoSigner.Hash(tx) + if hash != expectedHash { + t.Fatalf("Tx hash mismatches, get %s expect %s", hash, expectedHash) + } + + hash = latestSigner.Hash(tx) + if hash != expectedHash { + t.Fatalf("Tx hash mismatches, get %s expect %s", hash, expectedHash) + } + + tx, err = SignTx(tx, mikoSigner, sender) + if err != nil { + t.Fatalf("Failed to sign tx, err %s", err) + } + v, r, s := tx.RawSignatureValues() + transactionHash := tx.Hash() + + expectedTxHash := prefixedRlpHash(0x64, []interface{}{ + big.NewInt(2020), + innerTx.Nonce, + innerTx.GasTipCap, + innerTx.GasFeeCap, + innerTx.Gas, + innerTx.To, + innerTx.Value, + innerTx.Data, + innerTx.ExpiredTime, + innerTx.PayerV, + innerTx.PayerR, + innerTx.PayerS, + v, r, s, + }) + if transactionHash != expectedTxHash { + t.Fatalf("Transaction hash mismatches, get %s expect %s", transactionHash, expectedTxHash) + } + + // 2. Check sender + tx, err = SignTx(tx, mikoSigner, sender) + if err != nil { + t.Fatalf("Failed to sign tx, err %s", err) + } + + v, _, _ = tx.RawSignatureValues() + if v.Cmp(big.NewInt(0)) != 0 && v.Cmp(big.NewInt(1)) != 0 { + t.Fatalf("V is expected to be {0, 1}, get %d", v.Uint64()) + } + + recoveredSender, err := Sender(mikoSigner, tx) + if err != nil { + t.Fatalf("Failed to recover sender, err %s", err) + } + + if recoveredSender != senderAddr { + t.Fatalf("Sender mismatches, get %s expect %s", recoveredSender, senderAddr) + } + + // London signer should be able to recover sender too + recoveredSender, err = Sender(latestSigner, tx) + if err != nil { + t.Fatalf("Failed to recover sender, err %s", err) + } + + if recoveredSender != senderAddr { + t.Fatalf("Sender mismatches, get %s expect %s", recoveredSender, senderAddr) + } + + // EIP155 should not accept sponsored tx + _, err = Sender(eip155Signer, tx) + if err == nil || !errors.Is(err, ErrTxTypeNotSupported) { + t.Fatalf("Expect %s, get %s", ErrTxTypeNotSupported, err) + } + + // 3. Check payer + payerV, _, _ := tx.RawPayerSignatureValues() + if payerV.Cmp(big.NewInt(0)) != 0 && payerV.Cmp(big.NewInt(1)) != 0 { + t.Fatalf("payerV is expected to be {0, 1}, get %d", v.Uint64()) + } + recoveredPayerAddr, err := Payer(mikoSigner, tx) + if err != nil { + t.Fatalf("Failed to recover payer, err %s", err) + } + + if recoveredPayerAddr != payerAddr { + t.Fatalf("Payer mismatches, get %s expect %s", recoveredPayerAddr, payerAddr) + } + + // London signer should be able to recover sender too + recoveredPayerAddr, err = Payer(latestSigner, tx) + if err != nil { + t.Fatalf("Failed to recover payer, err %s", err) + } + + if recoveredPayerAddr != payerAddr { + t.Fatalf("Payer mismatches, get %s expect %s", recoveredPayerAddr, payerAddr) + } + + // 4. Check chainID + // Don't bother to fix up the signature here, Sender reject the + // tx with mismatching chainID before recovering address + innerTx.ChainID = big.NewInt(1000) + tx = NewTx(&innerTx) + + _, err = Sender(mikoSigner, tx) + if err == nil || !errors.Is(err, ErrInvalidChainId) { + t.Fatalf("Expect %s, get %s", ErrInvalidChainId, err) + } +} diff --git a/core/types/transaction_test.go b/core/types/transaction_test.go index 58c95071b2..2c124559ee 100644 --- a/core/types/transaction_test.go +++ b/core/types/transaction_test.go @@ -419,7 +419,7 @@ func TestTransactionCoding(t *testing.T) { ) for i := uint64(0); i < 500; i++ { var txdata TxData - switch i % 5 { + switch i % 6 { case 0: // Legacy tx. txdata = &LegacyTx{ @@ -467,6 +467,23 @@ func TestTransactionCoding(t *testing.T) { GasPrice: big.NewInt(10), AccessList: accesses, } + case 5: + // Sponsored tx + itx := SponsoredTx{ + ChainID: big.NewInt(1), + Nonce: i, + To: &recipient, + Gas: 123457, + GasTipCap: big.NewInt(10), + GasFeeCap: big.NewInt(10), + Data: []byte("abcdef"), + ExpiredTime: 100000, + } + txdata = &itx + itx.PayerR, itx.PayerS, itx.PayerV, err = PayerSign(key, signer, crypto.PubkeyToAddress(key.PublicKey), txdata) + if err != nil { + t.Fatal(err) + } } tx, err := SignNewTx(key, signer, txdata) if err != nil { diff --git a/ethclient/signer.go b/ethclient/signer.go index f827d4eb56..ac1ce39e9f 100644 --- a/ethclient/signer.go +++ b/ethclient/signer.go @@ -60,3 +60,6 @@ func (s *senderFromServer) Hash(tx *types.Transaction) common.Hash { func (s *senderFromServer) SignatureValues(tx *types.Transaction, sig []byte) (R, S, V *big.Int, err error) { panic("can't sign with senderFromServer") } +func (s *senderFromServer) Payer(tx *types.Transaction) (common.Address, error) { + panic("can't sign with senderFromServer") +} diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 219388ee12..04db451ceb 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -20,11 +20,12 @@ import ( "context" "errors" "fmt" - "github.com/ethereum/go-ethereum/eth/tracers/logger" "math/big" "strings" "time" + "github.com/ethereum/go-ethereum/eth/tracers/logger" + "github.com/davecgh/go-spew/spew" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/abi" @@ -1332,6 +1333,10 @@ type RPCTransaction struct { V *hexutil.Big `json:"v"` R *hexutil.Big `json:"r"` S *hexutil.Big `json:"s"` + ExpiredTime *hexutil.Uint64 `json:"expiredTime,omitempty"` + PayerV *hexutil.Big `json:"payerV,omitempty"` + PayerR *hexutil.Big `json:"payerR,omitempty"` + PayerS *hexutil.Big `json:"payerS,omitempty"` } // newRPCTransaction returns a transaction that will serialize to the RPC @@ -1378,6 +1383,24 @@ func newRPCTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber } else { result.GasPrice = (*hexutil.Big)(tx.GasFeeCap()) } + case types.SponsoredTxType: + result.ChainID = (*hexutil.Big)(tx.ChainId()) + result.GasFeeCap = (*hexutil.Big)(tx.GasFeeCap()) + result.GasTipCap = (*hexutil.Big)(tx.GasTipCap()) + // if the transaction has been mined, compute the effective gas price + if baseFee != nil && blockHash != (common.Hash{}) { + // price = min(tip + baseFee, gasFeeCap) + price := math.BigMin(new(big.Int).Add(tx.GasTipCap(), baseFee), tx.GasFeeCap()) + result.GasPrice = (*hexutil.Big)(price) + } else { + result.GasPrice = (*hexutil.Big)(tx.GasFeeCap()) + } + expiredTime := tx.ExpiredTime() + result.ExpiredTime = (*hexutil.Uint64)(&expiredTime) + v, r, s := tx.RawPayerSignatureValues() + result.PayerR = (*hexutil.Big)(r) + result.PayerS = (*hexutil.Big)(s) + result.PayerV = (*hexutil.Big)(v) } return result } @@ -1679,6 +1702,11 @@ func (s *PublicTransactionPoolAPI) GetTransactionReceipt(ctx context.Context, ha "logsBloom": receipt.Bloom, "type": hexutil.Uint(tx.Type()), } + if tx.Type() == types.SponsoredTxType { + payer, _ := types.Payer(signer, tx) + fields["payer"] = payer + } + // Assign the effective gas price paid if !s.b.ChainConfig().IsLondon(bigblock) { fields["effectiveGasPrice"] = hexutil.Uint64(tx.GasPrice().Uint64()) diff --git a/params/config.go b/params/config.go index a723737fbd..ef2450a30b 100644 --- a/params/config.go +++ b/params/config.go @@ -274,6 +274,10 @@ var ( PuffyBlock: big.NewInt(0), BubaBlock: big.NewInt(0), OlekBlock: big.NewInt(24935500), + ShillinBlock: big.NewInt(28825400), + AntennaBlock: big.NewInt(28825400), + // TODO: Fill this + MikoBlock: nil, } RoninTestnetBlacklistContract = common.HexToAddress("0xF53EED5210c9cF308abFe66bA7CF14884c95A8aC") @@ -318,6 +322,8 @@ var ( OlekBlock: big.NewInt(16849000), ShillinBlock: big.NewInt(20268000), AntennaBlock: big.NewInt(20737258), + // TODO: Fill this + MikoBlock: nil, } // GoerliTrustedCheckpoint contains the light client trusted checkpoint for the Görli test network. @@ -360,6 +366,7 @@ var ( PetersburgBlock: big.NewInt(0), IstanbulBlock: big.NewInt(0), MuirGlacierBlock: big.NewInt(0), + MikoBlock: big.NewInt(0), BerlinBlock: big.NewInt(0), LondonBlock: big.NewInt(0), ArrowGlacierBlock: nil, @@ -395,6 +402,7 @@ var ( PetersburgBlock: big.NewInt(0), IstanbulBlock: big.NewInt(0), MuirGlacierBlock: big.NewInt(0), + MikoBlock: big.NewInt(0), BerlinBlock: big.NewInt(0), LondonBlock: big.NewInt(0), ArrowGlacierBlock: nil, @@ -425,6 +433,7 @@ var ( PetersburgBlock: big.NewInt(0), IstanbulBlock: big.NewInt(0), MuirGlacierBlock: big.NewInt(0), + MikoBlock: big.NewInt(0), BerlinBlock: big.NewInt(0), LondonBlock: big.NewInt(0), ArrowGlacierBlock: nil, @@ -531,7 +540,10 @@ type ChainConfig struct { // Shillin hardfork introduces fast finality ShillinBlock *big.Int `json:"shillinBlock,omitempty"` // Shillin switch block (nil = no fork, 0 = already on activated) - AntennaBlock *big.Int `json:"antennaBlock,omitempty"` // AntennaBlock switch block (nil = no fork, 0 = already on activated) + AntennaBlock *big.Int `json:"antennaBlock,omitempty"` // AntennaBlock switch block (nil = no fork, 0 = already on activated) + // Miko hardfork introduces sponsored transactions + MikoBlock *big.Int `json:"mikoBlock,omitempty"` // Miko switch block (nil = no fork, 0 = already on activated) + BlacklistContractAddress *common.Address `json:"blacklistContractAddress,omitempty"` // Address of Blacklist Contract (nil = no blacklist) FenixValidatorContractAddress *common.Address `json:"fenixValidatorContractAddress,omitempty"` // Address of Ronin Contract in the Fenix hardfork (nil = no blacklist) WhiteListDeployerContractV2Address *common.Address `json:"whiteListDeployerContractV2Address,omitempty"` // Address of Whitelist Ronin Contract V2 (nil = no blacklist) @@ -650,7 +662,7 @@ func (c *ChainConfig) String() string { chainConfigFmt += "Petersburg: %v Istanbul: %v, Odysseus: %v, Fenix: %v, Muir Glacier: %v, Berlin: %v, London: %v, Arrow Glacier: %v, " chainConfigFmt += "Engine: %v, Blacklist Contract: %v, Fenix Validator Contract: %v, ConsortiumV2: %v, ConsortiumV2.RoninValidatorSet: %v, " chainConfigFmt += "ConsortiumV2.SlashIndicator: %v, ConsortiumV2.StakingContract: %v, Puffy: %v, Buba: %v, Olek: %v, Shillin: %v, Antenna: %v, " - chainConfigFmt += "ConsortiumV2.ProfileContract: %v, ConsortiumV2.FinalityTracking: %v, whiteListDeployerContractV2Address: %v}" + chainConfigFmt += "ConsortiumV2.ProfileContract: %v, ConsortiumV2.FinalityTracking: %v, whiteListDeployerContractV2Address: %v, Miko: %v}" return fmt.Sprintf(chainConfigFmt, c.ChainID, @@ -685,6 +697,7 @@ func (c *ChainConfig) String() string { profileContract.Hex(), finalityTrackingContract.Hex(), whiteListDeployerContractV2Address.Hex(), + c.MikoBlock, ) } @@ -808,6 +821,11 @@ func (c *ChainConfig) IsShillin(num *big.Int) bool { return isForked(c.ShillinBlock, num) } +// IsMiko returns whether the num is equals to or larger than the miko fork block. +func (c *ChainConfig) IsMiko(num *big.Int) bool { + return isForked(c.MikoBlock, num) +} + // CheckCompatible checks whether scheduled fork transitions have been imported // with a mismatching chain configuration. func (c *ChainConfig) CheckCompatible(newcfg *ChainConfig, height uint64) *ConfigCompatError { @@ -945,6 +963,9 @@ func (c *ChainConfig) checkCompatible(newcfg *ChainConfig, head *big.Int) *Confi if isForkIncompatible(c.AntennaBlock, newcfg.AntennaBlock, head) { return newCompatError("Antenna fork block", c.AntennaBlock, newcfg.AntennaBlock) } + if isForkIncompatible(c.MikoBlock, newcfg.MikoBlock, head) { + return newCompatError("Miko fork block", c.MikoBlock, newcfg.MikoBlock) + } return nil } @@ -1014,6 +1035,7 @@ type Rules struct { IsByzantium, IsConstantinople, IsPetersburg, IsIstanbul bool IsBerlin, IsLondon bool IsOdysseusFork, IsFenix, IsConsortiumV2, IsAntenna bool + IsMiko bool } // Rules ensures c's ChainID is not nil. @@ -1038,5 +1060,6 @@ func (c *ChainConfig) Rules(num *big.Int) Rules { IsFenix: c.IsFenix(num), IsConsortiumV2: c.IsConsortiumV2(num), IsAntenna: c.IsAntenna(num), + IsMiko: c.IsMiko(num), } }