From 7924cefb7e3e753d19fdabfef0604c74d90f7c7b Mon Sep 17 00:00:00 2001 From: Daniel Liu Date: Fri, 17 May 2024 17:49:58 +0800 Subject: [PATCH] core: add new eip-1559 tx constraints (#22970) --- core/chain_makers.go | 15 ++ core/error.go | 12 ++ core/state_processor_test.go | 290 +++++++++++++++++++++++++++++++++++ core/state_transition.go | 34 ++-- core/tx_pool.go | 11 +- core/tx_pool_test.go | 20 +++ 6 files changed, 368 insertions(+), 14 deletions(-) create mode 100644 core/state_processor_test.go diff --git a/core/chain_makers.go b/core/chain_makers.go index 32c9dcaca38c..4c0a21a4f164 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -328,3 +328,18 @@ func makeBlockChain(parent *types.Block, n int, engine consensus.Engine, db ethd }) return blocks } + +type fakeChainReader struct { + config *params.ChainConfig +} + +// Config returns the chain configuration. +func (cr *fakeChainReader) Config() *params.ChainConfig { + return cr.config +} + +func (cr *fakeChainReader) CurrentHeader() *types.Header { return nil } +func (cr *fakeChainReader) GetHeaderByNumber(number uint64) *types.Header { return nil } +func (cr *fakeChainReader) GetHeaderByHash(hash common.Hash) *types.Header { return nil } +func (cr *fakeChainReader) GetHeader(hash common.Hash, number uint64) *types.Header { return nil } +func (cr *fakeChainReader) GetBlock(hash common.Hash, number uint64) *types.Block { return nil } diff --git a/core/error.go b/core/error.go index fd45fdc73d3a..192d41778f15 100644 --- a/core/error.go +++ b/core/error.go @@ -54,6 +54,18 @@ var ( // ErrGasUintOverflow is returned when calculating gas usage. ErrGasUintOverflow = errors.New("gas uint64 overflow") + // ErrTipAboveFeeCap is a sanity error to ensure no one is able to specify a + // transaction with a tip higher than the total fee cap. + ErrTipAboveFeeCap = errors.New("tip higher than fee cap") + + // ErrTipVeryHigh is a sanity error to avoid extremely big numbers specified + // in the tip field. + ErrTipVeryHigh = errors.New("tip higher than 2^256-1") + + // ErrFeeCapVeryHigh is a sanity error to avoid extremely big numbers specified + // in the fee cap field. + ErrFeeCapVeryHigh = errors.New("fee cap higher than 2^256-1") + // ErrFeeCapTooLow is returned if the transaction fee cap is less than the // the base fee of the block. ErrFeeCapTooLow = errors.New("fee cap less than block base fee") diff --git a/core/state_processor_test.go b/core/state_processor_test.go new file mode 100644 index 000000000000..1327471c6a90 --- /dev/null +++ b/core/state_processor_test.go @@ -0,0 +1,290 @@ +// Copyright 2020 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "math/big" + "testing" + + "github.com/XinFinOrg/XDPoSChain/common" + "github.com/XinFinOrg/XDPoSChain/consensus" + "github.com/XinFinOrg/XDPoSChain/consensus/ethash" + "github.com/XinFinOrg/XDPoSChain/core/rawdb" + "github.com/XinFinOrg/XDPoSChain/core/types" + "github.com/XinFinOrg/XDPoSChain/core/vm" + "github.com/XinFinOrg/XDPoSChain/crypto" + "github.com/XinFinOrg/XDPoSChain/params" + "golang.org/x/crypto/sha3" +) + +// TestStateProcessorErrors tests the output from the 'core' errors +// as defined in core/error.go. These errors are generated when the +// blockchain imports bad blocks, meaning blocks which have valid headers but +// contain invalid transactions +func TestStateProcessorErrors(t *testing.T) { + var ( + config = ¶ms.ChainConfig{ + ChainId: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + Eip1559Block: big.NewInt(0), + Ethash: new(params.EthashConfig), + } + signer = types.LatestSigner(config) + testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + ) + var makeTx = func(nonce uint64, to common.Address, amount *big.Int, gasLimit uint64, gasPrice *big.Int, data []byte) *types.Transaction { + tx := types.NewTransaction(nonce, to, amount, gasLimit, gasPrice, data) + signedTx, err := types.SignTx(tx, signer, testKey) + if err != nil { + t.Fatalf("fail to sign tx: %v, err: %v", tx, err) + } + return signedTx + } + var mkDynamicTx = func(nonce uint64, to common.Address, gasLimit uint64, tip, feeCap *big.Int) *types.Transaction { + tx, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: nonce, + Tip: tip, + FeeCap: feeCap, + Gas: gasLimit, + To: &to, + Value: big.NewInt(0), + }), signer, testKey) + return tx + } + { // Tests against a 'recent' chain definition + var ( + db = rawdb.NewMemoryDatabase() + gspec = &Genesis{ + Config: config, + Alloc: GenesisAlloc{ + common.HexToAddress("0x71562b71999873DB5b286dF957af199Ec94617F7"): GenesisAccount{ + Balance: big.NewInt(1000000000000000000), // 1 ether + Nonce: 0, + }, + }, + } + genesis = gspec.MustCommit(db) + blockchain, _ = NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}) + ) + defer blockchain.Stop() + bigNumber := new(big.Int).SetBytes(common.FromHex("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")) + tooBigNumber := new(big.Int).Set(bigNumber) + tooBigNumber.Add(tooBigNumber, common.Big1) + for i, tt := range []struct { + txs []*types.Transaction + want string + }{ + { // ErrNonceTooLow + txs: []*types.Transaction{ + makeTx(0, common.Address{}, big.NewInt(0), params.TxGas, big.NewInt(875000000), nil), + makeTx(0, common.Address{}, big.NewInt(0), params.TxGas, big.NewInt(875000000), nil), + }, + want: "nonce too low: address xdc71562b71999873DB5b286dF957af199Ec94617F7, tx: 0 state: 1", + }, + { // ErrNonceTooHigh + txs: []*types.Transaction{ + makeTx(100, common.Address{}, big.NewInt(0), params.TxGas, big.NewInt(875000000), nil), + }, + want: "nonce too high: address xdc71562b71999873DB5b286dF957af199Ec94617F7, tx: 100 state: 0", + }, + { // ErrGasLimitReached + txs: []*types.Transaction{ + makeTx(0, common.Address{}, big.NewInt(0), 21000000, big.NewInt(875000000), nil), + }, + want: "gas limit reached", + }, + { // ErrInsufficientFundsForTransfer + txs: []*types.Transaction{ + makeTx(0, common.Address{}, big.NewInt(1000000000000000000), params.TxGas, big.NewInt(875000000), nil), + }, + want: "insufficient funds for transfer: address xdc71562b71999873DB5b286dF957af199Ec94617F7", + }, + { // ErrInsufficientFunds + txs: []*types.Transaction{ + makeTx(0, common.Address{}, big.NewInt(0), params.TxGas, big.NewInt(900000000000000000), nil), + }, + want: "insufficient funds for gas * price + value: address xdc71562b71999873DB5b286dF957af199Ec94617F7 have 1000000000000000000 want 18900000000000000000000", + }, + // ErrGasUintOverflow + // One missing 'core' error is ErrGasUintOverflow: "gas uint64 overflow", + // In order to trigger that one, we'd have to allocate a _huge_ chunk of data, such that the + // multiplication len(data) +gas_per_byte overflows uint64. Not testable at the moment + { // ErrIntrinsicGas + txs: []*types.Transaction{ + makeTx(0, common.Address{}, big.NewInt(0), params.TxGas-1000, big.NewInt(875000000), nil), + }, + want: "intrinsic gas too low: have 20000, want 21000", + }, + { // ErrGasLimitReached + txs: []*types.Transaction{ + makeTx(0, common.Address{}, big.NewInt(0), params.TxGas*1000, big.NewInt(875000000), nil), + }, + want: "gas limit reached", + }, + { // ErrFeeCapTooLow + txs: []*types.Transaction{ + mkDynamicTx(0, common.Address{}, params.TxGas, big.NewInt(0), big.NewInt(0)), + }, + want: "fee cap less than block base fee: address xdc71562b71999873DB5b286dF957af199Ec94617F7, feeCap: 0 baseFee: 875000000", + }, + { // ErrTipVeryHigh + txs: []*types.Transaction{ + mkDynamicTx(0, common.Address{}, params.TxGas, tooBigNumber, big.NewInt(1)), + }, + want: "tip higher than 2^256-1: address xdc71562b71999873DB5b286dF957af199Ec94617F7, tip bit length: 257", + }, + { // ErrFeeCapVeryHigh + txs: []*types.Transaction{ + mkDynamicTx(0, common.Address{}, params.TxGas, big.NewInt(1), tooBigNumber), + }, + want: "fee cap higher than 2^256-1: address xdc71562b71999873DB5b286dF957af199Ec94617F7, feeCap bit length: 257", + }, + { // ErrTipAboveFeeCap + txs: []*types.Transaction{ + mkDynamicTx(0, common.Address{}, params.TxGas, big.NewInt(2), big.NewInt(1)), + }, + want: "tip higher than fee cap: address xdc71562b71999873DB5b286dF957af199Ec94617F7, tip: 1, feeCap: 2", + }, + { // ErrInsufficientFunds + // Available balance: 1000000000000000000 + // Effective cost: 18375000021000 + // FeeCap * gas: 1050000000000000000 + // This test is designed to have the effective cost be covered by the balance, but + // the extended requirement on FeeCap*gas < balance to fail + txs: []*types.Transaction{ + mkDynamicTx(0, common.Address{}, params.TxGas, big.NewInt(1), big.NewInt(50000000000000)), + }, + want: "insufficient funds for gas * price + value: address xdc71562b71999873DB5b286dF957af199Ec94617F7 have 1000000000000000000 want 1050000000000000000", + }, + { // Another ErrInsufficientFunds, this one to ensure that feecap/tip of max u256 is allowed + txs: []*types.Transaction{ + mkDynamicTx(0, common.Address{}, params.TxGas, bigNumber, bigNumber), + }, + want: "insufficient funds for gas * price + value: address xdc71562b71999873DB5b286dF957af199Ec94617F7 have 1000000000000000000 want 2431633873983640103894990685182446064918669677978451844828609264166175722438635000", + }, + }[8:] { + block := GenerateBadBlock(t, genesis, ethash.NewFaker(), tt.txs, gspec.Config) + _, err := blockchain.InsertChain(types.Blocks{block}) + if err == nil { + t.Fatal("block imported without errors") + } + if have, want := err.Error(), tt.want; have != want { + t.Errorf("test %d:\nhave \"%v\"\nwant \"%v\"\n", i, have, want) + } + } + } + + // One final error is ErrTxTypeNotSupported. For this, we need an older chain + { + var ( + db = rawdb.NewMemoryDatabase() + gspec = &Genesis{ + Config: ¶ms.ChainConfig{ + ChainId: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + }, + Alloc: GenesisAlloc{ + common.HexToAddress("0x71562b71999873DB5b286dF957af199Ec94617F7"): GenesisAccount{ + Balance: big.NewInt(1000000000000000000), // 1 ether + Nonce: 0, + }, + }, + } + genesis = gspec.MustCommit(db) + blockchain, _ = NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}) + ) + defer blockchain.Stop() + for i, tt := range []struct { + txs []*types.Transaction + want string + }{ + { // ErrTxTypeNotSupported + txs: []*types.Transaction{ + mkDynamicTx(0, common.Address{}, params.TxGas-1000, big.NewInt(0), big.NewInt(0)), + }, + want: "transaction type not supported", + }, + } { + block := GenerateBadBlock(t, genesis, ethash.NewFaker(), tt.txs, gspec.Config) + _, err := blockchain.InsertChain(types.Blocks{block}) + if err == nil { + t.Fatal("block imported without errors") + } + if have, want := err.Error(), tt.want; have != want { + t.Errorf("test %d:\nhave \"%v\"\nwant \"%v\"\n", i, have, want) + } + } + } +} + +// GenerateBadBlock constructs a "block" which contains the transactions. The transactions are not expected to be +// valid, and no proper post-state can be made. But from the perspective of the blockchain, the block is sufficiently +// valid to be considered for import: +// - valid pow (fake), ancestry, difficulty, gaslimit etc +func GenerateBadBlock(t *testing.T, parent *types.Block, engine consensus.Engine, txs types.Transactions, config *params.ChainConfig) *types.Block { + header := &types.Header{ + ParentHash: parent.Hash(), + Coinbase: parent.Coinbase(), + Difficulty: engine.CalcDifficulty(&fakeChainReader{config}, parent.Time().Uint64()+10, &types.Header{ + Number: parent.Number(), + Time: parent.Time(), + Difficulty: parent.Difficulty(), + UncleHash: parent.UncleHash(), + }), + GasLimit: parent.GasLimit(), + Number: new(big.Int).Add(parent.Number(), common.Big1), + Time: new(big.Int).SetUint64(parent.Time().Uint64() + 10), + UncleHash: types.EmptyUncleHash, + } + if config.IsEIP1559(header.Number) { + header.BaseFee = common.BaseFee + } + var receipts []*types.Receipt + // The post-state result doesn't need to be correct (this is a bad block), but we do need something there + // Preferably something unique. So let's use a combo of blocknum + txhash + hasher := sha3.NewLegacyKeccak256() + hasher.Write(header.Number.Bytes()) + var cumulativeGas uint64 + for _, tx := range txs { + txh := tx.Hash() + hasher.Write(txh[:]) + receipt := types.NewReceipt(nil, false, cumulativeGas+tx.Gas()) + receipt.TxHash = tx.Hash() + receipt.GasUsed = tx.Gas() + receipts = append(receipts, receipt) + cumulativeGas += tx.Gas() + } + header.Root = common.BytesToHash(hasher.Sum(nil)) + // Assemble and return the final block for sealing + return types.NewBlock(header, txs, nil, receipts) +} diff --git a/core/state_transition.go b/core/state_transition.go index 186f88e1179c..b09a73bd6dd9 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -178,15 +178,17 @@ func (st *StateTransition) to() vm.AccountRef { } func (st *StateTransition) buyGas() error { - var ( - state = st.state - balanceTokenFee = st.balanceTokenFee() - from = st.from() - ) - mgval := new(big.Int).Mul(new(big.Int).SetUint64(st.msg.Gas()), st.gasPrice) + mgval := new(big.Int).SetUint64(st.msg.Gas()) + mgval = mgval.Mul(mgval, st.gasPrice) + balanceTokenFee := st.balanceTokenFee() if balanceTokenFee == nil { - if state.GetBalance(from.Address()).Cmp(mgval) < 0 { - return errInsufficientBalanceForGas + balanceCheck := mgval + if st.feeCap != nil { + balanceCheck = new(big.Int).SetUint64(st.msg.Gas()) + balanceCheck = balanceCheck.Mul(balanceCheck, st.feeCap) + } + 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) } } else if balanceTokenFee.Cmp(mgval) < 0 { return errInsufficientBalanceForGas @@ -198,7 +200,7 @@ func (st *StateTransition) buyGas() error { st.initialGas = st.msg.Gas() if balanceTokenFee == nil { - state.SubBalance(from.Address(), mgval) + st.state.SubBalance(st.msg.From(), mgval) } return nil } @@ -221,6 +223,18 @@ func (st *StateTransition) preCheck() error { } // Make sure that transaction feeCap is greater than the baseFee (post london) if st.evm.ChainConfig().IsEIP1559(st.evm.Context.BlockNumber) { + if l := st.feeCap.BitLen(); l > 256 { + return fmt.Errorf("%w: address %v, feeCap bit length: %d", ErrFeeCapVeryHigh, + msg.From().Hex(), l) + } + if l := st.tip.BitLen(); l > 256 { + return fmt.Errorf("%w: address %v, tip bit length: %d", ErrTipVeryHigh, + msg.From().Hex(), l) + } + if st.feeCap.Cmp(st.tip) < 0 { + return fmt.Errorf("%w: address %v, tip: %s, feeCap: %s", ErrTipAboveFeeCap, + msg.From().Hex(), st.feeCap, st.tip) + } // This will panic if baseFee is nil, but basefee presence is verified // as part of header validation. if st.feeCap.Cmp(st.evm.Context.BaseFee) < 0 { @@ -236,7 +250,7 @@ func (st *StateTransition) preCheck() error { // failed. An error indicates a consensus issue. func (st *StateTransition) TransitionDb(owner common.Address) (ret []byte, usedGas uint64, failed bool, err error, vmErr error) { if err = st.preCheck(); err != nil { - return + return nil, 0, false, err, nil } msg := st.msg sender := st.from() // err checked in preCheck diff --git a/core/tx_pool.go b/core/tx_pool.go index 795a84d6e8d1..359db728456f 100644 --- a/core/tx_pool.go +++ b/core/tx_pool.go @@ -106,10 +106,6 @@ var ( ErrDuplicateSpecialTransaction = errors.New("duplicate a special transaction") ErrMinDeploySMC = errors.New("smart contract creation cost is under allowance") - - // ErrTipAboveFeeCap is a sanity error to ensure no one is able to specify a - // transaction with a tip higher than the total fee cap. - ErrTipAboveFeeCap = errors.New("tip higher than fee cap") ) var ( @@ -616,6 +612,13 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error { if pool.currentMaxGas < tx.Gas() { return ErrGasLimit } + // Sanity check for extremely large numbers + if tx.FeeCap().BitLen() > 256 { + return ErrFeeCapVeryHigh + } + if tx.Tip().BitLen() > 256 { + return ErrTipVeryHigh + } // Ensure feeCap is less than or equal to tip. if tx.FeeCapIntCmp(tx.Tip()) < 0 { return ErrTipAboveFeeCap diff --git a/core/tx_pool_test.go b/core/tx_pool_test.go index f73d6f92bc8c..3a3766c26cbf 100644 --- a/core/tx_pool_test.go +++ b/core/tx_pool_test.go @@ -416,6 +416,26 @@ func TestTransactionTipAboveFeeCap(t *testing.T) { } } +func TestTransactionVeryHighValues(t *testing.T) { + t.Parallel() + + pool, key := setupTxPoolWithConfig(eip1559Config) + defer pool.Stop() + + veryBigNumber := big.NewInt(1) + veryBigNumber.Lsh(veryBigNumber, 300) + + tx := dynamicFeeTx(0, 100, big.NewInt(1), veryBigNumber, key) + if err := pool.AddRemote(tx); err != ErrTipVeryHigh { + t.Error("expected", ErrTipVeryHigh, "got", err) + } + + tx2 := dynamicFeeTx(0, 100, veryBigNumber, big.NewInt(1), key) + if err := pool.AddRemote(tx2); err != ErrFeeCapVeryHigh { + t.Error("expected", ErrFeeCapVeryHigh, "got", err) + } +} + func TestTransactionChainFork(t *testing.T) { t.Parallel()