diff --git a/cmd/XDC/config.go b/cmd/XDC/config.go index a9ba16c9dfc7..72e7f04a4727 100644 --- a/cmd/XDC/config.go +++ b/cmd/XDC/config.go @@ -21,7 +21,6 @@ import ( "errors" "fmt" "io" - "math/big" "os" "reflect" "runtime" @@ -43,7 +42,6 @@ import ( "github.com/XinFinOrg/XDPoSChain/log" "github.com/XinFinOrg/XDPoSChain/metrics" "github.com/XinFinOrg/XDPoSChain/node" - "github.com/XinFinOrg/XDPoSChain/params" "github.com/naoina/toml" "github.com/urfave/cli/v2" ) @@ -163,15 +161,6 @@ func loadBaseConfig(ctx *cli.Context) XDCConfig { common.Enable0xPrefix = false } - // Check GasPrice - common.MinGasPrice = big.NewInt(common.DefaultMinGasPrice) - if ctx.IsSet(utils.MinerGasPriceFlag.Name) { - if gasPrice := int64(ctx.Int(utils.MinerGasPriceFlag.Name)); gasPrice > common.DefaultMinGasPrice { - common.MinGasPrice = big.NewInt(gasPrice) - } - } - params.SetMinGasPrice50x(common.MinGasPrice) - // read passwords from environment passwords := []string{} for _, env := range cfg.Account.Passwords { diff --git a/common/constants.go b/common/constants.go index cc3a37f5aa4c..4b672715d0c4 100644 --- a/common/constants.go +++ b/common/constants.go @@ -40,8 +40,6 @@ var ( TRC21GasPriceBefore = big.NewInt(2500) TRC21GasPrice = big.NewInt(250000000) - MinGasPrice = big.NewInt(250000000) - BaseFee = big.NewInt(12500000000) // XDCx and XDCxlending BasePrice = big.NewInt(1000000000000000000) // 1 diff --git a/consensus/misc/eip1559/eip1559.go b/consensus/misc/eip1559/eip1559.go index b3f638b2b999..3864c9f02450 100644 --- a/consensus/misc/eip1559/eip1559.go +++ b/consensus/misc/eip1559/eip1559.go @@ -21,7 +21,6 @@ import ( "fmt" "math/big" - "github.com/XinFinOrg/XDPoSChain/common" "github.com/XinFinOrg/XDPoSChain/core/types" "github.com/XinFinOrg/XDPoSChain/params" ) @@ -56,12 +55,20 @@ func VerifyEip1559Header(config *params.ChainConfig, parent, header *types.Heade return nil } -// CalcBaseFee calculates the basefee of the header. +// CalcBaseFee computes the base fee for the block whose number is carried by header. +// +// NOTE: unlike upstream go-ethereum, header is the block being built or verified, +// not its parent. The XDC base fee does not depend on parent gas usage, but it is +// gas schedule aware, so passing a parent header yields the wrong tier on the +// block where a tier fork activates. func CalcBaseFee(config *params.ChainConfig, header *types.Header) *big.Int { - // If the current block is the first EIP-1559 block, return the InitialBaseFee. - if config.IsEIP1559(header.Number) { - return new(big.Int).Set(common.BaseFee) - } else { + return CalcBaseFeeForBlockNumber(config, header.Number) +} + +// CalcBaseFeeForBlockNumber computes the base fee for the given block number. +func CalcBaseFeeForBlockNumber(config *params.ChainConfig, number *big.Int) *big.Int { + if !config.IsEIP1559(number) { return nil } + return params.BaseFeeForBlock(config, number) } diff --git a/consensus/misc/eip1559/eip1559_test.go b/consensus/misc/eip1559/eip1559_test.go index 48e31dddd81a..ac95982e525f 100644 --- a/consensus/misc/eip1559/eip1559_test.go +++ b/consensus/misc/eip1559/eip1559_test.go @@ -20,7 +20,6 @@ import ( "math/big" "testing" - "github.com/XinFinOrg/XDPoSChain/common" "github.com/XinFinOrg/XDPoSChain/core/types" "github.com/XinFinOrg/XDPoSChain/params" ) @@ -31,6 +30,40 @@ func testConfigEip1559() *params.ChainConfig { return &config } +// TestCalcBaseFeeFollowsGasPriceSchedule pins the base fee returned for each +// tier of the gas price schedule. +func TestCalcBaseFeeFollowsGasPriceSchedule(t *testing.T) { + config := *params.TestChainConfig + config.EIP1559Block = big.NewInt(10) + config.Gas50xBlock = big.NewInt(0) + config.Gas2500xBlock = big.NewInt(20) + + for _, tc := range []struct { + name string + block int64 + want *big.Int + }{ + {name: "before eip1559", block: 9, want: nil}, + {name: "at eip1559", block: 10, want: big.NewInt(12_500_000_000)}, + {name: "below gas2500x", block: 19, want: big.NewInt(12_500_000_000)}, + {name: "at gas2500x", block: 20, want: big.NewInt(625_000_000_000)}, + {name: "above gas2500x", block: 21, want: big.NewInt(625_000_000_000)}, + } { + t.Run(tc.name, func(t *testing.T) { + got := CalcBaseFee(&config, &types.Header{Number: big.NewInt(tc.block)}) + if tc.want == nil { + if got != nil { + t.Fatalf("expected nil base fee, got %v", got) + } + return + } + if got == nil || got.Cmp(tc.want) != 0 { + t.Fatalf("unexpected base fee: have %v want %v", got, tc.want) + } + }) + } +} + func TestVerifyEip1559HeaderParentBaseFee(t *testing.T) { config := testConfigEip1559() @@ -52,7 +85,7 @@ func TestVerifyEip1559HeaderParentBaseFee(t *testing.T) { name: "eip1559 parent with basefee", parent: &types.Header{ Number: big.NewInt(1), - BaseFee: new(big.Int).Set(common.BaseFee), + BaseFee: new(big.Int).SetUint64(params.InitialBaseFee), }, headerNum: 2, wantOk: true, @@ -68,7 +101,7 @@ func TestVerifyEip1559HeaderParentBaseFee(t *testing.T) { } { header := &types.Header{ Number: big.NewInt(tc.headerNum), - BaseFee: new(big.Int).Set(common.BaseFee), + BaseFee: new(big.Int).SetUint64(params.InitialBaseFee), } err := VerifyEip1559Header(config, tc.parent, header) if tc.wantOk && err != nil { diff --git a/consensus/tests/engine_v1_tests/helper.go b/consensus/tests/engine_v1_tests/helper.go index 800e841c3f66..07e678720db0 100644 --- a/consensus/tests/engine_v1_tests/helper.go +++ b/consensus/tests/engine_v1_tests/helper.go @@ -417,7 +417,7 @@ func createBlockFromHeader(bc *core.BlockChain, customHeader *types.Header, txs Penalties: customHeader.Penalties, } if config != nil && config.IsEIP1559(header.Number) { - header.BaseFee = new(big.Int).Set(common.BaseFee) + header.BaseFee = params.BaseFeeForBlock(config, header.Number) } var block *types.Block if len(txs) == 0 { diff --git a/consensus/tests/engine_v2_tests/helper.go b/consensus/tests/engine_v2_tests/helper.go index e2cc5ffdc246..96922ee64f1f 100644 --- a/consensus/tests/engine_v2_tests/helper.go +++ b/consensus/tests/engine_v2_tests/helper.go @@ -958,7 +958,7 @@ func createBlockFromHeader(bc *core.BlockChain, customHeader *types.Header, txs Penalties: customHeader.Penalties, } if config != nil && config.IsEIP1559(header.Number) { - header.BaseFee = new(big.Int).Set(common.BaseFee) + header.BaseFee = params.BaseFeeForBlock(config, header.Number) } var block *types.Block if len(txs) == 0 { diff --git a/core/chainconfig_equal.go b/core/chainconfig_equal.go index 1aa43650281b..3da2bc8d7fbd 100644 --- a/core/chainconfig_equal.go +++ b/core/chainconfig_equal.go @@ -16,7 +16,7 @@ import ( // will stop matching the fast path and chainConfigJSONEqual will fall back to // field-level semantic comparison until both sides are re-encoded. Update the // pinned digest expectations in core/chainconfig_equal_test.go at the same time. -const chainConfigDigestVersion byte = 1 +const chainConfigDigestVersion byte = 2 func defaultHashChainConfigSemanticVersioned(cfg *params.ChainConfig) (byte, [32]byte) { return chainConfigDigestVersion, hashChainConfigSemantic(cfg) diff --git a/core/chainconfig_equal_test.go b/core/chainconfig_equal_test.go index 8a1e7938d8a6..764e89ca2209 100644 --- a/core/chainconfig_equal_test.go +++ b/core/chainconfig_equal_test.go @@ -29,6 +29,7 @@ var chainConfigDigestCoveredFields = []string{ "EIP158Block", "EIP1559Block", "Ethash", + "Gas2500xBlock", "Gas50xBlock", "HomesteadBlock", "IstanbulBlock", @@ -476,10 +477,10 @@ func TestHashChainConfigSemanticGoldenVectors(t *testing.T) { cfg *params.ChainConfig want string }{ - {name: "nil", cfg: nil, want: "47dc540c94ceb704a23875c11273e16bb0b8a87aed84de911f2133568115f254"}, - {name: "testnet", cfg: params.TestnetChainConfig.Clone(), want: "211f23893f2d69fcca32285082e8ee73f4231786cf5e66f1d62cccfa295aad76"}, - {name: "mainnet", cfg: params.XDCMainnetChainConfig.Clone(), want: "9076222c6783f37836c868190b78c7abe0b4d0d3fce93dc0aa12a6bc8b6ddcd3"}, - {name: "testnet-berlin-drift", cfg: testnetBerlinDrift, want: "9711b6f1f09247c095eb193be8f24977ca706bd9fac1db2b1441a49c0dc3922a"}, + {name: "nil", cfg: nil, want: "99be5efb88ca2013bd8e4eb035fd42d5245468fe9afa70d8ba9c1c419a48c4e8"}, + {name: "testnet", cfg: params.TestnetChainConfig.Clone(), want: "759186a62685d424dc8ca52ff0db8e5e298c0e9341344b3142dfd43b743ddbb7"}, + {name: "mainnet", cfg: params.XDCMainnetChainConfig.Clone(), want: "9b0ecfced732164016e61836ed57a626be9bffb3df283036e3337811a5feed3b"}, + {name: "testnet-berlin-drift", cfg: testnetBerlinDrift, want: "6465ddb5a31a1b15c30479e666d10cec03dd4a862b372b3ff3a3961ad2134041"}, } for _, test := range tests { diff --git a/core/genesis.go b/core/genesis.go index d5b0e68256d3..2049061b6692 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -778,7 +778,7 @@ func (g *Genesis) toBlockWithRoot(root common.Hash) *types.Block { if g.BaseFee != nil { head.BaseFee = g.BaseFee } else { - head.BaseFee = new(big.Int).SetUint64(params.InitialBaseFee) + head.BaseFee = params.BaseFeeForBlock(g.Config, common.Big0) } } return types.NewBlock(head, nil, nil, trie.NewStackTrie(nil)) diff --git a/core/genesis_test.go b/core/genesis_test.go index caff6949258c..bf673e605062 100644 --- a/core/genesis_test.go +++ b/core/genesis_test.go @@ -127,6 +127,7 @@ func TestCloneChainConfigDeepCopiesMigratedForkBlocks(t *testing.T) { TIPXDCXCancellationFeeBlock: big.NewInt(70), TIPTRC21FeeBlock: big.NewInt(75), Gas50xBlock: big.NewInt(77), + Gas2500xBlock: big.NewInt(78), BerlinBlock: big.NewInt(80), LondonBlock: big.NewInt(90), MergeBlock: big.NewInt(100), @@ -213,6 +214,31 @@ func TestGenesisCopyDeepCopiesChainConfig(t *testing.T) { } } +func TestGenesisToBlockUsesScheduledBaseFeeAtGenesis(t *testing.T) { + genesis := &Genesis{ + Config: ¶ms.ChainConfig{ + ChainID: big.NewInt(551), + TIPTRC21FeeBlock: big.NewInt(0), + Gas50xBlock: big.NewInt(0), + Gas2500xBlock: big.NewInt(0), + EIP1559Block: big.NewInt(0), + Ethash: new(params.EthashConfig), + }, + Alloc: types.GenesisAlloc{common.Address{1}: {Balance: big.NewInt(1)}}, + GasLimit: 4_700_000, + Difficulty: big.NewInt(1), + } + + block, err := genesis.ToBlockWithError() + if err != nil { + t.Fatalf("ToBlockWithError failed: %v", err) + } + want := new(big.Int).SetUint64(common.DefaultMinGasPrice * 2500) + if got := block.BaseFee(); got == nil || got.Cmp(want) != 0 { + t.Fatalf("unexpected genesis base fee: have %v want %v", got, want) + } +} + // TestHydrateProvidedChainConfigPreservesEngineLessBuiltInTestNetwork tests hydrate provided chain config preserves engine less built in test network. func TestHydrateProvidedChainConfigPreservesEngineLessBuiltInTestNetwork(t *testing.T) { futureFork := big.NewInt(1_000_000_000) diff --git a/core/state_processor_test.go b/core/state_processor_test.go index 13a55991551b..86314c5e0e4b 100644 --- a/core/state_processor_test.go +++ b/core/state_processor_test.go @@ -681,7 +681,7 @@ func GenerateBadBlock(t *testing.T, parent *types.Block, engine consensus.Engine UncleHash: types.EmptyUncleHash, } if config.IsEIP1559(header.Number) { - header.BaseFee = common.BaseFee + header.BaseFee = params.BaseFeeForBlock(config, header.Number) } 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 diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index e844d249edf8..062e074092fe 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -1351,8 +1351,9 @@ func (pool *LegacyPool) runReorg(done chan struct{}, reset *txpoolResetRequest, if reset != nil { pool.demoteUnexecutables() if reset.newHead != nil { - if pool.chainconfig.IsEIP1559(new(big.Int).Add(reset.newHead.Number, big.NewInt(1))) { - pendingBaseFee := eip1559.CalcBaseFee(pool.chainconfig, reset.newHead) + nextNumber := new(big.Int).Add(reset.newHead.Number, common.Big1) + if pool.chainconfig.IsEIP1559(nextNumber) { + pendingBaseFee := eip1559.CalcBaseFeeForBlockNumber(pool.chainconfig, nextNumber) pool.priced.SetBaseFee(pendingBaseFee) } else { pool.priced.Reheap() @@ -1495,12 +1496,12 @@ func (pool *LegacyPool) reset(oldHead, newHead *types.Header) { // future queue to the set of pending transactions. During this process, all // invalidated transactions (low nonce, low balance) are deleted. func (pool *LegacyPool) promoteExecutables(accounts []common.Address) []*types.Transaction { - gasLimit := pool.currentHead.Load().GasLimit - var number *big.Int - if head := pool.chain.CurrentHeader(); head != nil { - number = head.Number - } - promotable, dropped, removedAddresses := pool.queue.promoteExecutables(accounts, gasLimit, pool.currentState, pool.pendingNonces, pool.trc21FeeCapacity, number, pool.chainconfig) + head := pool.currentHead.Load() + gasLimit := head.GasLimit + // Pooled txs can only be included from the next block onwards, so affordability + // checks below resolve the gas schedule at that height. + nextNumber := new(big.Int).Add(head.Number, common.Big1) + promotable, dropped, removedAddresses := pool.queue.promoteExecutables(accounts, gasLimit, pool.currentState, pool.pendingNonces, pool.trc21FeeCapacity, nextNumber, pool.chainconfig) // promote all promotable transactions promoted := make([]*types.Transaction, 0, len(promotable)) @@ -1636,7 +1637,11 @@ func (pool *LegacyPool) truncateQueue() { // to trigger a re-heap is this function func (pool *LegacyPool) demoteUnexecutables() { // Iterate over all accounts and demote any non-executable transactions - gasLimit := pool.currentHead.Load().GasLimit + head := pool.currentHead.Load() + gasLimit := head.GasLimit + // Pending txs can only be included from the next block onwards, so affordability + // checks below resolve the gas schedule at that height. + nextNumber := new(big.Int).Add(head.Number, common.Big1) for addr, list := range pool.pending { nonce := pool.currentState.GetNonce(addr) @@ -1648,11 +1653,7 @@ func (pool *LegacyPool) demoteUnexecutables() { log.Trace("Removed old pending transaction", "hash", hash) } // Drop all transactions that are too costly (low balance or out of gas), and queue any invalids back for later - var number *big.Int = nil - if pool.chain.CurrentHeader() != nil { - number = pool.chain.CurrentHeader().Number - } - drops, invalids := list.Filter(pool.currentState.GetBalance(addr), gasLimit, pool.trc21FeeCapacity, number, pool.chainconfig) + drops, invalids := list.Filter(pool.currentState.GetBalance(addr), gasLimit, pool.trc21FeeCapacity, nextNumber, pool.chainconfig) for _, tx := range drops { hash := tx.Hash() pool.all.Remove(hash) diff --git a/core/txpool/legacypool/legacypool_test.go b/core/txpool/legacypool/legacypool_test.go index f86f10a7325e..c907a97f73f6 100644 --- a/core/txpool/legacypool/legacypool_test.go +++ b/core/txpool/legacypool/legacypool_test.go @@ -161,8 +161,65 @@ func TestSetupPoolUsesPreGas50xTestConfig(t *testing.T) { pool, _ := setupPool() defer pool.Close() - if got := params.GetMinGasPrice(pool.currentHead.Load().Number, pool.chainconfig); got.Cmp(common.MinGasPrice) != 0 { - t.Fatalf("unexpected min gas price for legacypool tests: have %v want %v", got, common.MinGasPrice) + want := big.NewInt(common.DefaultMinGasPrice) + if got := params.GetMinGasPrice(pool.currentHead.Load().Number, pool.chainconfig); got.Cmp(want) != 0 { + t.Fatalf("unexpected min gas price for legacypool tests: have %v want %v", got, want) + } +} + +// TestDemoteUnexecutablesUsesNextBlockGasSchedule verifies that pending TRC21 +// transactions are priced against the block they can first be included in. +func TestDemoteUnexecutablesUsesNextBlockGasSchedule(t *testing.T) { + t.Parallel() + + token := common.HexToAddress("0x00000000000000000000000000000000000000cc") + gas50xCapacity := new(big.Int).Mul(new(big.Int).Mul(common.TRC21GasPrice, big.NewInt(50)), new(big.Int).SetUint64(params.TxGas)) + + tests := []struct { + name string + head int64 + dropped bool + }{ + {name: "gas50x capacity kept before gas2500x boundary", head: 198, dropped: false}, + {name: "gas50x capacity dropped when next block reaches gas2500x", head: 199, dropped: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + pool, key := setupPoolWithConfig(¶ms.ChainConfig{ + ChainID: big.NewInt(1339), + Ethash: new(params.EthashConfig), + Gas50xBlock: big.NewInt(100), + Gas2500xBlock: big.NewInt(200), + }) + defer pool.Close() + + pool.mu.Lock() + defer pool.mu.Unlock() + + head := *pool.currentHead.Load() + head.Number = big.NewInt(tt.head) + pool.currentHead.Store(&head) + pool.trc21FeeCapacity = map[common.Address]*big.Int{token: gas50xCapacity} + + tx, err := types.SignTx(types.NewTransaction(0, token, big.NewInt(0), params.TxGas, big.NewInt(1), nil), types.HomesteadSigner{}, key) + if err != nil { + t.Fatalf("failed to sign transaction: %v", err) + } + from, _ := types.Sender(pool.signer, tx) + + list := newList(true) + list.Add(tx, pool.config.PriceBump) + pool.pending[from] = list + pool.all.Add(tx) + + pool.demoteUnexecutables() + + if dropped := pool.all.Get(tx.Hash()) == nil; dropped != tt.dropped { + t.Fatalf("unexpected drop decision at head %d: have %v want %v", tt.head, dropped, tt.dropped) + } + }) } } @@ -249,7 +306,7 @@ type unsignedAuth struct { } func setCodeTx(nonce uint64, key *ecdsa.PrivateKey, unsigned []unsignedAuth) *types.Transaction { - return pricedSetCodeTx(nonce, 250000, uint256.MustFromBig(common.MinGasPrice), uint256.NewInt(1), key, unsigned) + return pricedSetCodeTx(nonce, 250000, uint256.NewInt(common.DefaultMinGasPrice), uint256.NewInt(1), key, unsigned) } func pricedSetCodeTx(nonce uint64, gaslimit uint64, gasFee, tip *uint256.Int, key *ecdsa.PrivateKey, unsigned []unsignedAuth) *types.Transaction { @@ -629,7 +686,7 @@ func TestSpecialTxReplacementThroughPool(t *testing.T) { // A nonzero price keeps validation simple; list.Add bypasses the price-bump // rules for special txs regardless of the actual price. - price := new(big.Int).Add(new(big.Int).Set(common.MinGasPrice), big.NewInt(1)) + price := big.NewInt(common.DefaultMinGasPrice + 1) mkSpecial := func(key *ecdsa.PrivateKey, payload []byte) *types.Transaction { tx, err := types.SignTx(types.NewTransaction(0, common.BlockSignersBinary, big.NewInt(0), 100000, price, payload), types.HomesteadSigner{}, key) if err != nil { @@ -768,7 +825,7 @@ func TestPromoteExecutablesQueueEmptyWithoutReservation(t *testing.T) { defer pool.Close() <-pool.initDoneCh - queuedTx := pricedTransaction(5, 100000, new(big.Int).Add(new(big.Int).Set(common.MinGasPrice), big.NewInt(1)), key) + queuedTx := pricedTransaction(5, 100000, big.NewInt(common.DefaultMinGasPrice+1), key) if err := pool.addRemoteSync(queuedTx); err != nil { t.Fatalf("failed to add queued tx: %v", err) } @@ -2865,7 +2922,7 @@ func TestSetCodeTransactions(t *testing.T) { testAddBalance(pool, addrB, big.NewInt(params.Ether)) testAddBalance(pool, addrC, big.NewInt(params.Ether)) - minGasPrice := new(big.Int).Set(common.MinGasPrice) + minGasPrice := big.NewInt(common.DefaultMinGasPrice) minGasFee := uint256.MustFromBig(minGasPrice) tripleGasFee := new(uint256.Int).Mul(new(uint256.Int).Set(minGasFee), uint256.NewInt(3)) legacyReplacePrice := new(big.Int).Mul(minGasPrice, big.NewInt(10)) @@ -3163,9 +3220,9 @@ func TestSetCodeTransactionsReorg(t *testing.T) { ) testAddBalance(pool, addrA, big.NewInt(params.Ether)) - minGasFee := uint256.MustFromBig(common.MinGasPrice) + minGasFee := uint256.NewInt(common.DefaultMinGasPrice) doubleGasFee := new(uint256.Int).Mul(new(uint256.Int).Set(minGasFee), uint256.NewInt(2)) - legacyPrice := new(big.Int).Mul(new(big.Int).Set(common.MinGasPrice), big.NewInt(10)) + legacyPrice := big.NewInt(common.DefaultMinGasPrice * 10) // Send an authorization for 0x42 var authList []types.SetCodeAuthorization @@ -3328,7 +3385,7 @@ func TestPendingMinTipThreshold(t *testing.T) { addr := crypto.PubkeyToAddress(key.PublicKey) testAddBalance(pool, addr, big.NewInt(1_000_000_000_000_000)) - threshold := new(big.Int).Add(new(big.Int).Set(common.MinGasPrice), big.NewInt(100)) + threshold := big.NewInt(common.DefaultMinGasPrice + 100) aboveThreshold := new(big.Int).Set(threshold) belowThreshold := new(big.Int).Sub(new(big.Int).Set(threshold), big.NewInt(1)) @@ -3368,7 +3425,7 @@ func TestPendingMinTipWithBaseFee(t *testing.T) { addr := crypto.PubkeyToAddress(key.PublicKey) testAddBalance(pool, addr, big.NewInt(1_000_000_000_000_000)) - minGasTip := new(big.Int).Set(common.MinGasPrice) + minGasTip := big.NewInt(common.DefaultMinGasPrice) tipPass := new(big.Int).Add(new(big.Int).Set(minGasTip), big.NewInt(80)) tipPassWithoutBaseFeeOnly := new(big.Int).Add(new(big.Int).Set(minGasTip), big.NewInt(60)) @@ -3413,7 +3470,7 @@ func TestPendingKeepsLocalAndSpecialTransactions(t *testing.T) { pool, _ := setupPool() defer pool.Close() - minGasTip := new(big.Int).Set(common.MinGasPrice) + minGasTip := big.NewInt(common.DefaultMinGasPrice) filterTip := new(big.Int).Add(new(big.Int).Set(minGasTip), big.NewInt(100)) specialKey, _ := crypto.GenerateKey() @@ -3472,7 +3529,7 @@ func TestPendingDynamicFeeThresholdWithoutBaseFee(t *testing.T) { addr := crypto.PubkeyToAddress(key.PublicKey) testAddBalance(pool, addr, big.NewInt(1_000_000_000_000_000)) - minTipBig := new(big.Int).Add(new(big.Int).Set(common.MinGasPrice), big.NewInt(50)) + minTipBig := big.NewInt(common.DefaultMinGasPrice + 50) equalTip := new(big.Int).Set(minTipBig) belowTip := new(big.Int).Sub(new(big.Int).Set(minTipBig), big.NewInt(1)) diff --git a/core/txpool/validation.go b/core/txpool/validation.go index 90de3641749c..14f253632de3 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -236,13 +236,19 @@ func ValidateTransactionWithState(tx *types.Transaction, signer types.Signer, op number = opts.CurrentNumber() to = tx.To() ) + // A pooled tx can only be included from the next block onwards, so gas + // schedule lookups below resolve the fork tier at that height. + pendingNumber := number + if number != nil { + pendingNumber = new(big.Int).Add(number, common.Big1) + } if to != nil { if value, ok := opts.Trc21FeeCapacity[*to]; ok { feeCapacity = value if !opts.State.ValidateTRC21Tx(from, *to, tx.Data()) { return core.ErrInsufficientFunds } - cost = tx.TxCost(number, opts.Config) + cost = tx.TxCost(pendingNumber, opts.Config) } } newBalance := new(big.Int).Add(balance, feeCapacity) @@ -288,7 +294,7 @@ func ValidateTransactionWithState(tx *types.Transaction, signer types.Signer, op // Validate gas price if !tx.IsSpecialTransaction() { - minGasPrice := params.GetMinGasPrice(number, opts.Config) + minGasPrice := params.GetMinGasPrice(pendingNumber, opts.Config) if tx.GasPrice().Cmp(minGasPrice) < 0 { return ErrUnderMinGasPrice } diff --git a/core/txpool/validation_denylist_test.go b/core/txpool/validation_denylist_test.go index a425a95e02d7..9ce5e1ee6de8 100644 --- a/core/txpool/validation_denylist_test.go +++ b/core/txpool/validation_denylist_test.go @@ -40,7 +40,7 @@ func newValidationStateOpts(t *testing.T, cfg *params.ChainConfig, number *big.I statedb.AddBalance(from, new(big.Int).Mul(big.NewInt(1_000_000), big.NewInt(params.Ether)), tracing.BalanceChangeUnspecified) denylistedReceiver := common.HexToAddress("0x5248bfb72fd4f234e062d3e9bb76f08643004fcd") - gasPrice := new(big.Int).Mul(new(big.Int).Set(common.MinGasPrice), big.NewInt(10)) + gasPrice := big.NewInt(common.DefaultMinGasPrice * 10) tx, err := types.SignTx( types.NewTransaction(0, denylistedReceiver, big.NewInt(1), params.TxGas, gasPrice, nil), types.HomesteadSigner{}, @@ -108,7 +108,7 @@ func TestValidateTransactionWithStateDenylistHardForkBoundaries(t *testing.T) { from := crypto.PubkeyToAddress(key.PublicKey) statedb.AddBalance(from, new(big.Int).Mul(big.NewInt(1_000_000), big.NewInt(params.Ether)), tracing.BalanceChangeUnspecified) - gasPrice := new(big.Int).Mul(new(big.Int).Set(common.MinGasPrice), big.NewInt(10)) + gasPrice := big.NewInt(common.DefaultMinGasPrice * 10) tx, err := types.SignTx( types.NewTransaction(0, common.HexToAddress("0x00000000000000000000000000000000000000b1"), big.NewInt(1), params.TxGas, gasPrice, nil), types.HomesteadSigner{}, diff --git a/core/txpool/validation_mingasprice_test.go b/core/txpool/validation_mingasprice_test.go new file mode 100644 index 000000000000..f64944eedc9f --- /dev/null +++ b/core/txpool/validation_mingasprice_test.go @@ -0,0 +1,143 @@ +package txpool + +import ( + "errors" + "math/big" + "testing" + + "github.com/XinFinOrg/XDPoSChain/common" + "github.com/XinFinOrg/XDPoSChain/core" + "github.com/XinFinOrg/XDPoSChain/core/rawdb" + "github.com/XinFinOrg/XDPoSChain/core/state" + "github.com/XinFinOrg/XDPoSChain/core/tracing" + "github.com/XinFinOrg/XDPoSChain/core/types" + "github.com/XinFinOrg/XDPoSChain/crypto" + "github.com/XinFinOrg/XDPoSChain/params" +) + +// TestValidateTransactionWithStateTRC21CostSchedule checks that the TRC21 fee +// capacity check prices a pending tx with the next block's gas schedule. +func TestValidateTransactionWithStateTRC21CostSchedule(t *testing.T) { + cfg := ¶ms.ChainConfig{Gas50xBlock: big.NewInt(100), Gas2500xBlock: big.NewInt(200)} + + token := common.HexToAddress("0x00000000000000000000000000000000000000cc") + // Fee capacity that exactly covers the transaction at the Gas50x tier. + gas50xCapacity := new(big.Int).Mul( + new(big.Int).Mul(common.TRC21GasPrice, big.NewInt(50)), + new(big.Int).SetUint64(params.TxGas), + ) + + for _, tc := range []struct { + name string + number *big.Int + wantErr error + }{ + {name: "gas50x capacity accepted before gas2500x boundary", number: big.NewInt(198)}, + {name: "gas50x capacity rejected when next block reaches gas2500x", number: big.NewInt(199), wantErr: core.ErrInsufficientFunds}, + } { + t.Run(tc.name, func(t *testing.T) { + statedb, err := state.NewWithChainConfig(types.EmptyRootHash, state.NewDatabase(rawdb.NewMemoryDatabase()), cfg) + if err != nil { + t.Fatalf("failed to create state: %v", err) + } + key, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("failed to generate key: %v", err) + } + + // The sender is left without native balance so that the TRC21 fee + // capacity alone decides the outcome. + tx, err := types.SignTx( + types.NewTransaction(0, token, big.NewInt(0), params.TxGas, big.NewInt(common.DefaultMinGasPrice*1000), []byte{0x01, 0x02, 0x03, 0x04}), + types.HomesteadSigner{}, + key, + ) + if err != nil { + t.Fatalf("failed to sign tx: %v", err) + } + + opts := &ValidationOptionsWithState{ + Config: cfg, + State: statedb, + Trc21FeeCapacity: map[common.Address]*big.Int{token: gas50xCapacity}, + ExistingExpenditure: func(common.Address) *big.Int { return new(big.Int) }, + ExistingCost: func(common.Address, uint64) *big.Int { return nil }, + PendingNonce: func(common.Address) uint64 { return 0 }, + CurrentNumber: func() *big.Int { return tc.number }, + } + + err = ValidateTransactionWithState(tx, types.HomesteadSigner{}, opts) + if tc.wantErr == nil { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + return + } + if !errors.Is(err, tc.wantErr) { + t.Fatalf("unexpected error: have %v want %v", err, tc.wantErr) + } + }) + } +} + +// TestValidateTransactionWithStateMinGasPriceSchedule checks that the pool +// admission floor for pending txs follows the next block's gas schedule. +func TestValidateTransactionWithStateMinGasPriceSchedule(t *testing.T) { + cfg := ¶ms.ChainConfig{Gas50xBlock: big.NewInt(100), Gas2500xBlock: big.NewInt(200)} + + gas50xPrice := big.NewInt(common.DefaultMinGasPrice * 50) + gas2500xPrice := big.NewInt(common.DefaultMinGasPrice * 2500) + + for _, tc := range []struct { + name string + number *big.Int + gasPrice *big.Int + wantErr error + }{ + {name: "gas50x floor accepted before gas2500x boundary", number: big.NewInt(198), gasPrice: gas50xPrice}, + {name: "gas50x floor rejected when next block reaches gas2500x", number: big.NewInt(199), gasPrice: gas50xPrice, wantErr: ErrUnderMinGasPrice}, + {name: "gas2500x floor accepted at gas2500x", number: big.NewInt(200), gasPrice: gas2500xPrice}, + } { + t.Run(tc.name, func(t *testing.T) { + statedb, err := state.NewWithChainConfig(types.EmptyRootHash, state.NewDatabase(rawdb.NewMemoryDatabase()), cfg) + if err != nil { + t.Fatalf("failed to create state: %v", err) + } + key, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("failed to generate key: %v", err) + } + from := crypto.PubkeyToAddress(key.PublicKey) + statedb.AddBalance(from, new(big.Int).Mul(big.NewInt(1_000_000), big.NewInt(params.Ether)), tracing.BalanceChangeUnspecified) + + tx, err := types.SignTx( + types.NewTransaction(0, common.HexToAddress("0x1234"), big.NewInt(1), params.TxGas, tc.gasPrice, nil), + types.HomesteadSigner{}, + key, + ) + if err != nil { + t.Fatalf("failed to sign tx: %v", err) + } + + opts := &ValidationOptionsWithState{ + Config: cfg, + State: statedb, + ExistingExpenditure: func(common.Address) *big.Int { return new(big.Int) }, + ExistingCost: func(common.Address, uint64) *big.Int { return nil }, + PendingNonce: func(common.Address) uint64 { return 0 }, + CurrentNumber: func() *big.Int { return tc.number }, + } + + err = ValidateTransactionWithState(tx, types.HomesteadSigner{}, opts) + if tc.wantErr == nil { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + return + } + if err != tc.wantErr { + t.Fatalf("unexpected error: have %v want %v", err, tc.wantErr) + } + }) + } +} diff --git a/core/vm/eips.go b/core/vm/eips.go index f5f96e32d96f..367c25f9c067 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -202,10 +202,18 @@ func opTstore(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { return nil, nil } +// baseFeeForOpcode is what BASEFEE reports when the header carries no base fee. +// Precomputed because the London-to-EIP1559 window spans millions of blocks. +var baseFeeForOpcode = *uint256.MustFromBig(params.BaseFeeForOpcode()) + // opBaseFee implements BASEFEE opcode func opBaseFee(pc *uint64, evm *EVM, callContext *ScopeContext) ([]byte, error) { - baseFee, _ := uint256.FromBig(common.BaseFee) - callContext.Stack.push(baseFee) + if value := evm.Context.BaseFee; value != nil { + baseFee, _ := uint256.FromBig(value) + callContext.Stack.push(baseFee) + return nil, nil + } + callContext.Stack.push(&baseFeeForOpcode) return nil, nil } diff --git a/core/vm/evm.go b/core/vm/evm.go index 548cf2d8765c..308036239013 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -64,7 +64,7 @@ type BlockContext struct { BlockNumber *big.Int // Provides information for NUMBER Time uint64 // Provides information for TIME Difficulty *big.Int // Provides information for DIFFICULTY - BaseFee *big.Int // Provides information for BASEFEE (0 if vm runs with NoBaseFee flag and 0 gas price) + BaseFee *big.Int // Provides information for BASEFEE, nil before EIP-1559 Random *common.Hash // Provides information for PREVRANDAO } @@ -73,7 +73,7 @@ type BlockContext struct { type TxContext struct { // Message information Origin common.Address // Provides information for ORIGIN - GasPrice *big.Int // Provides information for GASPRICE (and is used to zero the basefee if NoBaseFee is set) + GasPrice *big.Int // Provides information for GASPRICE } // EVM is the Ethereum Virtual Machine base object and provides diff --git a/core/vm/instructions_test.go b/core/vm/instructions_test.go index 681b52ee009c..27539f3349dc 100644 --- a/core/vm/instructions_test.go +++ b/core/vm/instructions_test.go @@ -698,6 +698,86 @@ func TestCreate2Addresses(t *testing.T) { } } +// TestBaseFee checks that BASEFEE reports the header base fee when the block +// carries one, and otherwise falls back to the pinned opcode value, which +// covers the London-to-EIP1559 window where XDC leaves the header field unset +// but the opcode is already available. +func TestBaseFee(t *testing.T) { + gasSchedule := func(gas50x, gas2500x int64) *params.ChainConfig { + cfg := *params.TestChainConfig + cfg.Gas50xBlock = big.NewInt(gas50x) + if gas2500x >= 0 { + cfg.Gas2500xBlock = big.NewInt(gas2500x) + } + return &cfg + } + for _, tt := range []struct { + name string + config *params.ChainConfig + number *big.Int + baseFee *big.Int + want *big.Int + }{ + {name: "header base fee wins over schedule", config: gasSchedule(0, 0), number: big.NewInt(100), baseFee: big.NewInt(7), want: big.NewInt(7)}, + {name: "absent before any tier", config: gasSchedule(200, -1), number: big.NewInt(100), want: new(big.Int).SetUint64(params.InitialBaseFee)}, + {name: "absent on gas50x tier", config: gasSchedule(0, -1), number: big.NewInt(100), want: new(big.Int).SetUint64(params.InitialBaseFee)}, + {name: "absent on gas2500x tier stays pinned", config: gasSchedule(0, 50), number: big.NewInt(100), want: new(big.Int).SetUint64(params.InitialBaseFee)}, + {name: "absent with unset block number", config: gasSchedule(0, 0), want: new(big.Int).SetUint64(params.InitialBaseFee)}, + } { + t.Run(tt.name, func(t *testing.T) { + var ( + evm = NewEVM(BlockContext{BlockNumber: tt.number, BaseFee: tt.baseFee}, nil, nil, tt.config, Config{}) + stack = newstack() + pc = uint64(0) + ) + opBaseFee(&pc, evm, &ScopeContext{nil, stack, nil}) + if len(stack.data) != 1 { + t.Fatalf("expected one item on stack, got %d", len(stack.data)) + } + actual := stack.pop() + expected, overflow := uint256.FromBig(tt.want) + if overflow { + t.Fatal("invalid overflow") + } + if actual.Cmp(expected) != 0 { + t.Fatalf("unexpected base fee: have %x want %x", &actual, expected) + } + }) + } +} + +// TestBaseFeeFallbackSurvivesStackMutation guards that the precomputed BASEFEE +// fallback is not corrupted by in-place stack mutations. Stack.push copies the +// pushed value into the stack slice, so an opcode mutating its operand in place +// (e.g. opAdd) must not leak the change back into the shared baseFeeForOpcode. +// If Stack ever switches to storing pointers, this test must fail loudly. +func TestBaseFeeFallbackSurvivesStackMutation(t *testing.T) { + cfg := *params.TestChainConfig + cfg.Gas50xBlock = big.NewInt(0) + + evm := NewEVM(BlockContext{BlockNumber: big.NewInt(100)}, nil, nil, &cfg, Config{}) + stack := newstack() + scope := &ScopeContext{nil, stack, nil} + pc := uint64(0) + + // BASEFEE, constant 1, ADD: with the BASEFEE result as the mutated operand + // this is exactly the sequence a contract computing block.basefee + 1 runs. + opBaseFee(&pc, evm, scope) + stack.push(uint256.NewInt(1)) + opAdd(&pc, evm, scope) + + // A second BASEFEE must still report the pinned InitialBaseFee. + opBaseFee(&pc, evm, scope) + got := stack.pop() + want, overflow := uint256.FromBig(new(big.Int).SetUint64(params.InitialBaseFee)) + if overflow { + t.Fatal("unexpected overflow") + } + if got.Cmp(want) != 0 { + t.Fatalf("baseFeeForOpcode corrupted by stack mutation: have %v want %v", &got, want) + } +} + func TestRandom(t *testing.T) { type testcase struct { name string diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 5126a7e27653..789964f6f53b 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -28,7 +28,7 @@ import ( // Config are the configuration options for the Interpreter type Config struct { Tracer *tracing.Hooks - NoBaseFee bool // Forces the EIP-1559 baseFee to 0 (needed for 0 price calls) + NoBaseFee bool // Skips the base fee check and fee payment for zero-priced calls EnablePreimageRecording bool // Enables recording of SHA3/keccak preimages ExtraEips []int // Additional EIPS that are to be enabled } diff --git a/eth/gasprice/feehistory.go b/eth/gasprice/feehistory.go index 10a3716447fd..4aef32136bd6 100644 --- a/eth/gasprice/feehistory.go +++ b/eth/gasprice/feehistory.go @@ -84,8 +84,9 @@ func (oracle *Oracle) processBlock(bf *blockFees, percentiles []float64) { if bf.results.baseFee = bf.header.BaseFee; bf.results.baseFee == nil { bf.results.baseFee = new(big.Int) } - if chainconfig.IsEIP1559(big.NewInt(int64(bf.blockNumber + 1))) { - bf.results.nextBaseFee = eip1559.CalcBaseFee(chainconfig, bf.header) + nextNumber := new(big.Int).SetUint64(bf.blockNumber + 1) + if chainconfig.IsEIP1559(nextNumber) { + bf.results.nextBaseFee = eip1559.CalcBaseFeeForBlockNumber(chainconfig, nextNumber) } else { bf.results.nextBaseFee = new(big.Int) } diff --git a/eth/gasprice/gasprice.go b/eth/gasprice/gasprice.go index a3ae759ffd0d..b17d7929cf70 100644 --- a/eth/gasprice/gasprice.go +++ b/eth/gasprice/gasprice.go @@ -226,7 +226,10 @@ func (oracle *Oracle) SuggestTipCap(ctx context.Context) (*big.Int, error) { // Check min gas price for non-eip1559 block if head.BaseFee == nil { - minGasPrice := params.GetMinGasPrice(head.Number, oracle.backend.ChainConfig()) + // The suggested price is for a transaction that can only be included from + // the next block onwards, so resolve the gas schedule at that height. + nextNumber := new(big.Int).Add(head.Number, common.Big1) + minGasPrice := params.GetMinGasPrice(nextNumber, oracle.backend.ChainConfig()) if price.Cmp(minGasPrice) < 0 { price = new(big.Int).Set(minGasPrice) } diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index a297559f7bb9..89bae5c05a06 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -92,7 +92,10 @@ func (s *EthereumAPI) GasPrice(ctx context.Context) (*hexutil.Big, error) { return nil, err } if head := s.b.CurrentHeader(); head.BaseFee != nil { - tipcap.Add(tipcap, head.BaseFee) + // The quoted price is for a transaction that can only be included from the + // next block onwards, so resolve the gas schedule at that height. + nextNumber := new(big.Int).Add(head.Number, common.Big1) + tipcap.Add(tipcap, params.BaseFeeForBlock(s.b.ChainConfig(), nextNumber)) } return (*hexutil.Big)(tipcap), err } @@ -1660,7 +1663,8 @@ func newRPCPendingTransaction(tx *types.Transaction, current *types.Header, conf blockTime = uint64(0) ) if current != nil { - baseFee = eip1559.CalcBaseFee(config, current) + nextNumber := new(big.Int).Add(current.Number, common.Big1) + baseFee = eip1559.CalcBaseFeeForBlockNumber(config, nextNumber) blockNumber = current.Number.Uint64() blockTime = current.Time } diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 317b9f94adb2..eed8f87d3792 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -4279,9 +4279,11 @@ func TestEthereumAPIBasic(t *testing.T) { backend.current = &types.Header{Number: big.NewInt(1100), BaseFee: big.NewInt(10)} api := NewEthereumAPI(backend) + // The quoted price is tip plus the base fee scheduled for the next block. gasPrice, err := api.GasPrice(context.Background()) require.NoError(t, err) - require.Equal(t, (*hexutil.Big)(big.NewInt(52)), gasPrice) + wantGasPrice := new(big.Int).Add(big.NewInt(42), params.BaseFeeForBlock(backend.ChainConfig(), big.NewInt(1101))) + require.Equal(t, (*hexutil.Big)(wantGasPrice), gasPrice) tip, err := api.MaxPriorityFeePerGas(context.Background()) require.NoError(t, err) diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go index 7c8c2778b493..4568dea5c0db 100644 --- a/internal/ethapi/simulate.go +++ b/internal/ethapi/simulate.go @@ -156,13 +156,13 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, // Set header fields that depend only on parent block. // Parent hash is needed for evm.GetHashFn to work. header.ParentHash = parent.Hash() - if sim.chainConfig.IsLondon(header.Number) { + if sim.chainConfig.IsEIP1559(header.Number) { // In non-validation mode base fee is set to 0 if it is not overridden. // This is because it creates an edge case in EVM where gasPrice < baseFee. // Base fee could have been overridden. if header.BaseFee == nil { if sim.validate { - header.BaseFee = eip1559.CalcBaseFee(sim.chainConfig, parent) + header.BaseFee = eip1559.CalcBaseFee(sim.chainConfig, header) } else { header.BaseFee = big.NewInt(0) } diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index effc6779c065..85ec259f486b 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -253,6 +253,7 @@ func newBackendMock() *backendMock { PetersburgBlock: big.NewInt(0), IstanbulBlock: big.NewInt(0), TIPTRC21FeeBlock: big.NewInt(0), + Gas50xBlock: big.NewInt(0), BerlinBlock: big.NewInt(0), EIP1559Block: big.NewInt(1000), TRC21IssuerSMC: params.TestnetChainConfig.TRC21IssuerSMC, diff --git a/params/config.go b/params/config.go index 65598e378922..19c9cb6a96ba 100644 --- a/params/config.go +++ b/params/config.go @@ -94,6 +94,7 @@ type ChainConfig struct { TIPXDCXCancellationFeeBlock *big.Int `json:"tipXDCXCancellationFeeBlock,omitempty"` TIPTRC21FeeBlock *big.Int `json:"tipTRC21FeeBlock,omitempty"` Gas50xBlock *big.Int `json:"gas50xBlock,omitempty"` + Gas2500xBlock *big.Int `json:"gas2500xBlock,omitempty"` TIPXDCXMinerDisableBlock *big.Int `json:"tipXDCXMinerDisableBlock,omitempty"` TIPXDCXReceiverDisableBlock *big.Int `json:"tipXDCXReceiverDisableBlock,omitempty"` DynamicGasLimitBlock *big.Int `json:"dynamicGasLimitBlock,omitempty"` @@ -563,6 +564,19 @@ func (c *ChainConfig) CheckConfigForkOrder() error { return fmt.Errorf("invalid chain config: %w: %s %v > %s %v", ErrWrongForkSwitchOrder, rule.before.name, before, rule.after.name, after) } } + // The BASEFEE opcode is live from London, but XDC only fills the header base + // fee from EIP-1559 on; in between the opcode falls back to BaseFeeForOpcode, + // which is pinned to the Gas50x tier price. A tier above Gas50x taking effect + // inside that window would make the opcode under-report the schedule. + if c.Gas2500xBlock != nil && c.LondonBlock != nil { + windowStart := c.LondonBlock + if c.Gas2500xBlock.Cmp(windowStart) > 0 { + windowStart = c.Gas2500xBlock + } + if c.EIP1559Block == nil || c.EIP1559Block.Cmp(windowStart) > 0 { + return fmt.Errorf("invalid chain config: %w: Gas2500xBlock %v takes effect before EIP1559Block %v fills the header base fee", ErrWrongForkSwitchOrder, c.Gas2500xBlock, c.EIP1559Block) + } + } if c.XDPoS == nil && c.Ethash == nil && c.Clique == nil && !isBuiltInTestNetwork(c.ChainID) { return fmt.Errorf("invalid chain config: %w: %s", ErrMissingForkSwitch, "XDPoS") } @@ -698,6 +712,9 @@ func (c *ChainConfig) String() string { if c.Gas50xBlock != nil { result += fmt.Sprintf(", Gas50x: %v", c.Gas50xBlock) } + if c.Gas2500xBlock != nil { + result += fmt.Sprintf(", Gas2500x: %v", c.Gas2500xBlock) + } if c.TIPXDCXMinerDisableBlock != nil { result += fmt.Sprintf(", TIPXDCXMinerDisable: %v", c.TIPXDCXMinerDisableBlock) } @@ -795,6 +812,7 @@ func (c *ChainConfig) Description() string { banner += fmt.Sprintf(" - Merge: %-8v\n", c.MergeBlock) banner += fmt.Sprintf(" - Shanghai: %-8v\n", c.ShanghaiBlock) banner += fmt.Sprintf(" - Gas50x: %-8v\n", c.Gas50xBlock) + banner += fmt.Sprintf(" - Gas2500x: %-8v\n", c.Gas2500xBlock) banner += fmt.Sprintf(" - TIPXDCXMinerDisable: %-8v\n", c.TIPXDCXMinerDisableBlock) banner += fmt.Sprintf(" - TIPXDCXReceiverDisable: %-8v\n", c.TIPXDCXReceiverDisableBlock) banner += fmt.Sprintf(" - EIP1559: %-8v\n", c.EIP1559Block) @@ -841,7 +859,7 @@ func (c *ChainConfig) GatherForks() []uint64 { // ActiveForks returns the list of active forks at the given block height. // The returned list is sorted in alphabetical order. func (c *ChainConfig) ActiveForks(block *big.Int) []string { - activeForks := make([]string, 0, 36) + activeForks := make([]string, 0, 37) if c.IsBerlin(block) { activeForks = append(activeForks, "Berlin") } @@ -869,6 +887,9 @@ func (c *ChainConfig) ActiveForks(block *big.Int) []string { if c.IsEIP158(block) { activeForks = append(activeForks, "EIP158") } + if c.IsGas2500x(block) { + activeForks = append(activeForks, "Gas2500x") + } if c.IsGas50x(block) { activeForks = append(activeForks, "Gas50x") } diff --git a/params/config_backfill.go b/params/config_backfill.go index 2b5a9926fb4b..d94d55a8c931 100644 --- a/params/config_backfill.go +++ b/params/config_backfill.go @@ -143,6 +143,11 @@ var chainConfigForkOrderSpecialCaseRules = func() []chainConfigForkOrderSpecialC after: "Gas50xBlock", shouldValidate: func(before, after *big.Int) bool { return before != nil && after != nil }, }, + { + before: "Gas50xBlock", + after: "Gas2500xBlock", + shouldValidate: func(before, after *big.Int) bool { return before != nil && after != nil }, + }, { before: "Gas50xBlock", after: "TIPXDCXMinerDisableBlock", @@ -242,11 +247,17 @@ var chainConfigCompatibilitySpecialCaseFieldNames = func() map[string]struct{} { return fieldNames }() +// chainConfigCompatibilityInsertionDefs splices fork fields that are validated by +// special-case rules instead of chainConfigForkOrderFields into the compatibility +// scan order. This order only drives which mismatch checkCompatible reports first +// and is unrelated to the params/forks enum order. Entries sharing an after field +// are inserted in slice order. var chainConfigCompatibilityInsertionDefs = []struct { after string name string }{ {after: "ShanghaiBlock", name: "Gas50xBlock"}, + {after: "ShanghaiBlock", name: "Gas2500xBlock"}, } var chainConfigCompatibilityFields = func() []chainConfigBigIntField { diff --git a/params/config_backfill_fields.json b/params/config_backfill_fields.json index de220d200cd4..70dba835dad2 100644 --- a/params/config_backfill_fields.json +++ b/params/config_backfill_fields.json @@ -220,6 +220,12 @@ "customMigrated": true, "xdcSpecific": true }, + { + "name": "Gas2500xBlock", + "jsonKey": "gas2500xBlock", + "backfillSet": "builtin", + "xdcSpecific": true + }, { "name": "TIPXDCXMinerDisableBlock", "jsonKey": "tipXDCXMinerDisableBlock", diff --git a/params/config_backfill_generated.go b/params/config_backfill_generated.go index 4f64d44f583f..cecc3f2d6a1a 100644 --- a/params/config_backfill_generated.go +++ b/params/config_backfill_generated.go @@ -208,6 +208,13 @@ var generatedChainConfigBuiltInBackfillForkBlockFields = []chainConfigBigIntFiel get: func(c *ChainConfig) *big.Int { return c.Gas50xBlock }, bind: func(c *ChainConfig) **big.Int { return &c.Gas50xBlock }, }, + { + name: "Gas2500xBlock", + jsonKey: "gas2500xBlock", + xdcSpecific: true, + get: func(c *ChainConfig) *big.Int { return c.Gas2500xBlock }, + bind: func(c *ChainConfig) **big.Int { return &c.Gas2500xBlock }, + }, { name: "TIPXDCXMinerDisableBlock", jsonKey: "tipXDCXMinerDisableBlock", diff --git a/params/config_backfill_test.go b/params/config_backfill_test.go index f29060e9a32e..06731799dea7 100644 --- a/params/config_backfill_test.go +++ b/params/config_backfill_test.go @@ -137,6 +137,7 @@ func TestForEachChainConfigForkBlockCoversStandardAndXDCForks(t *testing.T) { "TIPXDCXCancellationFeeBlock", "TIPTRC21FeeBlock", "Gas50xBlock", + "Gas2500xBlock", "TIPXDCXMinerDisableBlock", "TIPXDCXReceiverDisableBlock", "DynamicGasLimitBlock", @@ -245,6 +246,7 @@ func TestForEachChainConfigForkOrderSpecialCaseRuleCoversGas50xConstraints(t *te want := []rule{ {before: "TIPTRC21FeeBlock", after: "Gas50xBlock"}, + {before: "Gas50xBlock", after: "Gas2500xBlock"}, {before: "Gas50xBlock", after: "TIPXDCXMinerDisableBlock"}, } diff --git a/params/config_compat_test.go b/params/config_compat_test.go index c0bef49a103e..351432cd6f03 100644 --- a/params/config_compat_test.go +++ b/params/config_compat_test.go @@ -448,6 +448,7 @@ func TestForEachChainConfigCompatibleForkBlockPairUsesForkValidationOrder(t *tes if name == "ShanghaiBlock" { want = append(want, name) want = append(want, "Gas50xBlock") + want = append(want, "Gas2500xBlock") return } if _, specialCase := chainConfigCompatibilitySpecialCaseFieldNames[name]; specialCase { diff --git a/params/config_forks.go b/params/config_forks.go index 5f596131cbd0..b6a33b237cdd 100644 --- a/params/config_forks.go +++ b/params/config_forks.go @@ -150,6 +150,11 @@ func (c *ChainConfig) IsGas50x(num *big.Int) bool { return isForked(c.Gas50xBlock, num) } +// IsGas2500x returns whether num is either equal to the Gas2500x fork block or greater. +func (c *ChainConfig) IsGas2500x(num *big.Int) bool { + return isForked(c.Gas2500xBlock, num) +} + // IsTIPXDCXMiner reports whether XDCX miner handling is active at num, taking // the later miner-disable fork into account. func (c *ChainConfig) IsTIPXDCXMiner(num *big.Int) bool { diff --git a/params/config_networks.go b/params/config_networks.go index ee500cdcb0bf..cbb97f217ab5 100644 --- a/params/config_networks.go +++ b/params/config_networks.go @@ -512,6 +512,7 @@ var ( MergeBlock: big.NewInt(0), ShanghaiBlock: big.NewInt(0), Gas50xBlock: big.NewInt(0), + Gas2500xBlock: big.NewInt(0), TIPXDCXMinerDisableBlock: big.NewInt(0), TIPXDCXReceiverDisableBlock: big.NewInt(0), EIP1559Block: big.NewInt(0), diff --git a/params/config_networks_test.go b/params/config_networks_test.go index 7887a3daea49..c8df0f4e71f6 100644 --- a/params/config_networks_test.go +++ b/params/config_networks_test.go @@ -320,6 +320,9 @@ func TestMainnetChainConfigDoesNotDeclareXDCSpecificFields(t *testing.T) { if MainnetChainConfig.Gas50xBlock != nil { t.Fatalf("expected MainnetChainConfig Gas50xBlock to be nil, have %v", MainnetChainConfig.Gas50xBlock) } + if MainnetChainConfig.Gas2500xBlock != nil { + t.Fatalf("expected MainnetChainConfig Gas2500xBlock to be nil, have %v", MainnetChainConfig.Gas2500xBlock) + } if MainnetChainConfig.TRC21IssuerSMC != (common.Address{}) { t.Fatalf("expected MainnetChainConfig TRC21IssuerSMC to be zero, have %s", MainnetChainConfig.TRC21IssuerSMC.Hex()) } @@ -343,3 +346,20 @@ func TestMainnetChainConfigDoesNotDeclareXDCSpecificFields(t *testing.T) { t.Fatal("expected XDCMainnetChainConfig TRC21IssuerSMC to remain configured") } } + +func TestDefaultXDCNetworksDoNotEnableGas2500xFork(t *testing.T) { + for _, tc := range []struct { + name string + cfg *ChainConfig + }{ + {name: "devnet", cfg: DevnetChainConfig}, + {name: "testnet", cfg: TestnetChainConfig}, + {name: "mainnet", cfg: XDCMainnetChainConfig}, + } { + t.Run(tc.name, func(t *testing.T) { + if tc.cfg.Gas2500xBlock != nil { + t.Fatalf("expected %s Gas2500xBlock to be nil, have %v", tc.name, tc.cfg.Gas2500xBlock) + } + }) + } +} diff --git a/params/config_test.go b/params/config_test.go index 78c7e3bd6dcd..8d60c5a99187 100644 --- a/params/config_test.go +++ b/params/config_test.go @@ -156,6 +156,178 @@ func TestChainConfigValidateForStartup(t *testing.T) { t.Fatalf("unexpected error string: %v", err) } }) + t.Run("gas2500x block must not precede gas50x block", func(t *testing.T) { + cfg := &ChainConfig{ + ChainID: big.NewInt(1234), + TIPTRC21FeeBlock: big.NewInt(0), + Gas50xBlock: big.NewInt(20), + Gas2500xBlock: big.NewInt(10), + TRC21IssuerSMC: TestnetChainConfig.TRC21IssuerSMC, + XDCXListingSMC: TestnetChainConfig.XDCXListingSMC, + RelayerRegistrationSMC: TestnetChainConfig.RelayerRegistrationSMC, + LendingRegistrationSMC: TestnetChainConfig.LendingRegistrationSMC, + Ethash: new(EthashConfig), + } + + err := cfg.CheckConfigForkOrder() + if !errors.Is(err, ErrWrongForkSwitchOrder) { + t.Fatalf("unexpected error: have %v want %v", err, ErrWrongForkSwitchOrder) + } + if err == nil || err.Error() != "invalid chain config: wrong fork switch order: Gas50xBlock 20 > Gas2500xBlock 10" { + t.Fatalf("unexpected error string: %v", err) + } + }) + t.Run("gas2500x block requires gas50x block", func(t *testing.T) { + cfg := &ChainConfig{ + ChainID: big.NewInt(1234), + TIPTRC21FeeBlock: big.NewInt(0), + Gas2500xBlock: big.NewInt(10), + TRC21IssuerSMC: TestnetChainConfig.TRC21IssuerSMC, + XDCXListingSMC: TestnetChainConfig.XDCXListingSMC, + RelayerRegistrationSMC: TestnetChainConfig.RelayerRegistrationSMC, + LendingRegistrationSMC: TestnetChainConfig.LendingRegistrationSMC, + Ethash: new(EthashConfig), + } + + err := cfg.CheckConfigForkOrder() + if !errors.Is(err, ErrMissingForkSwitch) { + t.Fatalf("unexpected error: have %v want %v", err, ErrMissingForkSwitch) + } + if err == nil || err.Error() != "invalid chain config: missing fork switch: Gas50xBlock" { + t.Fatalf("unexpected error string: %v", err) + } + }) + t.Run("nil gas2500x block is accepted", func(t *testing.T) { + cfg := &ChainConfig{ + ChainID: big.NewInt(1234), + TIPTRC21FeeBlock: big.NewInt(0), + Gas50xBlock: big.NewInt(20), + TRC21IssuerSMC: TestnetChainConfig.TRC21IssuerSMC, + XDCXListingSMC: TestnetChainConfig.XDCXListingSMC, + RelayerRegistrationSMC: TestnetChainConfig.RelayerRegistrationSMC, + LendingRegistrationSMC: TestnetChainConfig.LendingRegistrationSMC, + Ethash: new(EthashConfig), + } + + if err := cfg.CheckConfigForkOrder(); err != nil { + t.Fatalf("CheckConfigForkOrder rejected nil Gas2500xBlock: %v", err) + } + }) + t.Run("gas2500x block following eip1559 block is accepted", func(t *testing.T) { + cfg := &ChainConfig{ + ChainID: big.NewInt(1234), + TIPTRC21FeeBlock: big.NewInt(0), + Gas50xBlock: big.NewInt(10), + LondonBlock: big.NewInt(10), + EIP1559Block: big.NewInt(20), + Gas2500xBlock: big.NewInt(30), + TRC21IssuerSMC: TestnetChainConfig.TRC21IssuerSMC, + XDCXListingSMC: TestnetChainConfig.XDCXListingSMC, + RelayerRegistrationSMC: TestnetChainConfig.RelayerRegistrationSMC, + LendingRegistrationSMC: TestnetChainConfig.LendingRegistrationSMC, + Ethash: new(EthashConfig), + } + + if err := cfg.CheckConfigForkOrder(); err != nil { + t.Fatalf("CheckConfigForkOrder rejected gas2500x after eip1559: %v", err) + } + }) + t.Run("gas2500x block must not take effect inside the basefee opcode window", func(t *testing.T) { + cfg := &ChainConfig{ + ChainID: big.NewInt(1234), + TIPTRC21FeeBlock: big.NewInt(0), + Gas50xBlock: big.NewInt(10), + LondonBlock: big.NewInt(10), + Gas2500xBlock: big.NewInt(20), + EIP1559Block: big.NewInt(30), + TRC21IssuerSMC: TestnetChainConfig.TRC21IssuerSMC, + XDCXListingSMC: TestnetChainConfig.XDCXListingSMC, + RelayerRegistrationSMC: TestnetChainConfig.RelayerRegistrationSMC, + LendingRegistrationSMC: TestnetChainConfig.LendingRegistrationSMC, + Ethash: new(EthashConfig), + } + + err := cfg.CheckConfigForkOrder() + if !errors.Is(err, ErrWrongForkSwitchOrder) { + t.Fatalf("unexpected error: have %v want %v", err, ErrWrongForkSwitchOrder) + } + if err == nil || err.Error() != "invalid chain config: wrong fork switch order: Gas2500xBlock 20 takes effect before EIP1559Block 30 fills the header base fee" { + t.Fatalf("unexpected error string: %v", err) + } + }) + t.Run("gas2500x block below london block is rejected when eip1559 is later", func(t *testing.T) { + cfg := &ChainConfig{ + ChainID: big.NewInt(1234), + TIPTRC21FeeBlock: big.NewInt(0), + Gas50xBlock: big.NewInt(0), + Gas2500xBlock: big.NewInt(0), + LondonBlock: big.NewInt(10), + EIP1559Block: big.NewInt(20), + TRC21IssuerSMC: TestnetChainConfig.TRC21IssuerSMC, + XDCXListingSMC: TestnetChainConfig.XDCXListingSMC, + RelayerRegistrationSMC: TestnetChainConfig.RelayerRegistrationSMC, + LendingRegistrationSMC: TestnetChainConfig.LendingRegistrationSMC, + Ethash: new(EthashConfig), + } + + if err := cfg.CheckConfigForkOrder(); !errors.Is(err, ErrWrongForkSwitchOrder) { + t.Fatalf("unexpected error: have %v want %v", err, ErrWrongForkSwitchOrder) + } + }) + t.Run("gas2500x block is accepted when the basefee opcode window is empty", func(t *testing.T) { + cfg := &ChainConfig{ + ChainID: big.NewInt(1234), + TIPTRC21FeeBlock: big.NewInt(0), + Gas50xBlock: big.NewInt(0), + Gas2500xBlock: big.NewInt(0), + LondonBlock: big.NewInt(10), + EIP1559Block: big.NewInt(10), + TRC21IssuerSMC: TestnetChainConfig.TRC21IssuerSMC, + XDCXListingSMC: TestnetChainConfig.XDCXListingSMC, + RelayerRegistrationSMC: TestnetChainConfig.RelayerRegistrationSMC, + LendingRegistrationSMC: TestnetChainConfig.LendingRegistrationSMC, + Ethash: new(EthashConfig), + } + + if err := cfg.CheckConfigForkOrder(); err != nil { + t.Fatalf("CheckConfigForkOrder rejected an empty basefee opcode window: %v", err) + } + }) + t.Run("gas2500x block without eip1559 block is rejected", func(t *testing.T) { + cfg := &ChainConfig{ + ChainID: big.NewInt(1234), + TIPTRC21FeeBlock: big.NewInt(0), + Gas50xBlock: big.NewInt(10), + LondonBlock: big.NewInt(10), + Gas2500xBlock: big.NewInt(20), + TRC21IssuerSMC: TestnetChainConfig.TRC21IssuerSMC, + XDCXListingSMC: TestnetChainConfig.XDCXListingSMC, + RelayerRegistrationSMC: TestnetChainConfig.RelayerRegistrationSMC, + LendingRegistrationSMC: TestnetChainConfig.LendingRegistrationSMC, + Ethash: new(EthashConfig), + } + + if err := cfg.CheckConfigForkOrder(); !errors.Is(err, ErrWrongForkSwitchOrder) { + t.Fatalf("unexpected error: have %v want %v", err, ErrWrongForkSwitchOrder) + } + }) + t.Run("gas2500x block without london block is accepted", func(t *testing.T) { + cfg := &ChainConfig{ + ChainID: big.NewInt(1234), + TIPTRC21FeeBlock: big.NewInt(0), + Gas50xBlock: big.NewInt(10), + Gas2500xBlock: big.NewInt(20), + TRC21IssuerSMC: TestnetChainConfig.TRC21IssuerSMC, + XDCXListingSMC: TestnetChainConfig.XDCXListingSMC, + RelayerRegistrationSMC: TestnetChainConfig.RelayerRegistrationSMC, + LendingRegistrationSMC: TestnetChainConfig.LendingRegistrationSMC, + Ethash: new(EthashConfig), + } + + if err := cfg.CheckConfigForkOrder(); err != nil { + t.Fatalf("CheckConfigForkOrder rejected gas2500x without london: %v", err) + } + }) t.Run("tiptrc21 fee block requires system contract addresses", func(t *testing.T) { cfg := &ChainConfig{ ChainID: big.NewInt(1234), diff --git a/params/forks/forks.go b/params/forks/forks.go index 28a3f02cee30..e1f0983a1c30 100644 --- a/params/forks/forks.go +++ b/params/forks/forks.go @@ -21,6 +21,7 @@ import "fmt" // Fork is a numerical identifier of specific network upgrades (forks). type Fork int +// Forks are ordered by XDC mainnet activation height; unscheduled forks go last. const ( Frontier Fork = iota FrontierThawing @@ -60,6 +61,11 @@ const ( TIPEpochHalving Prague Osaka + Gas2500x + + // lastFork is a sentinel marking the end of the enum, not a real fork. + // It is only used by test cases, not referenced by the production code. + lastFork ) // String implements fmt.Stringer. @@ -110,4 +116,5 @@ var forkToString = map[Fork]string{ TIPEpochHalving: "TIPEpochHalving", Prague: "Prague", Osaka: "Osaka", + Gas2500x: "Gas2500x", } diff --git a/params/forks/forks_test.go b/params/forks/forks_test.go new file mode 100644 index 000000000000..a78eeb2ef8d4 --- /dev/null +++ b/params/forks/forks_test.go @@ -0,0 +1,39 @@ +// Copyright 2023 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 forks + +import "testing" + +// TestForkToStringCoversEveryFork guards against adding a fork constant without +// a label, which String would otherwise report as "Unknown fork (n)". +func TestForkToStringCoversEveryFork(t *testing.T) { + for f := Frontier; f < lastFork; f++ { + if _, ok := forkToString[f]; !ok { + t.Fatalf("fork %d has no label", int(f)) + } + } + if len(forkToString) != int(lastFork) { + t.Fatalf("forkToString has %d entries, want %d", len(forkToString), int(lastFork)) + } +} + +// TestGasTierForksAreOrdered pins the relative order of the gas schedule tiers. +func TestGasTierForksAreOrdered(t *testing.T) { + if Gas50x >= Gas2500x { + t.Fatalf("Gas50x (%d) must precede Gas2500x (%d)", Gas50x, Gas2500x) + } +} diff --git a/params/gas.go b/params/gas.go index 5932aab3493f..9dc8bc4ce895 100644 --- a/params/gas.go +++ b/params/gas.go @@ -8,55 +8,129 @@ import ( "github.com/XinFinOrg/XDPoSChain/log" ) -var gasPrice50x = big.NewInt(12500000000) -var minGasPrice50x = big.NewInt(12500000000) +// gasTier describes one step of the XDC gas price schedule. price is the only +// value that defines a tier: the consensus baseline common.DefaultMinGasPrice +// scaled by the tier multiplier, shared by chain-default gas price, TRC21 gas +// price and EIP-1559 base fee. It is computed once at init and never mutated, +// so accessors handing it to a caller must return a copy. +type gasTier struct { + price *big.Int + activated func(cfg *ChainConfig, number *big.Int) bool +} + +// gasTiers is ordered from the latest fork to the earliest, so resolution +// returns the first active entry. Introducing a new tier means adding one row. +var gasTiers = []gasTier{ + { + price: scaledGasPrice(2500), + activated: func(cfg *ChainConfig, number *big.Int) bool { return cfg.IsGas2500x(number) }, + }, + { + price: scaledGasPrice(50), + activated: func(cfg *ChainConfig, number *big.Int) bool { return cfg.IsGas50x(number) }, + }, +} + +// scaledGasPrice returns the schedule baseline scaled by multiplier. Every +// price in the schedule is derived from the immutable common.DefaultMinGasPrice +// rather than a mutable package variable because it feeds consensus critical +// values such as the EIP-1559 base fee. The pre-Gas50x baseline is multiplier 1. +// The product is computed in big.Int so a future tier cannot silently overflow. +func scaledGasPrice(multiplier int64) *big.Int { + return new(big.Int).Mul(big.NewInt(common.DefaultMinGasPrice), big.NewInt(multiplier)) +} + +// baselineGasPrice is the chain-default gas price of the pre-Gas50x baseline +// tier. Like the tier prices it is shared and must be copied before it escapes. +var baselineGasPrice = scaledGasPrice(1) + +// tierGasPrice returns the gas price of the tier active at number, or false +// when no tier fork has fired yet and the caller must fall back to its own +// pre-Gas50x baseline. The returned value is shared, so copy it before handing +// it to a caller. +func tierGasPrice(cfg *ChainConfig, number *big.Int) (*big.Int, bool) { + for _, tier := range gasTiers { + if tier.activated(cfg, number) { + return tier.price, true + } + } + return nil, false +} + +// baselineTRC21GasPrice returns the TRC21 gas price of the pre-Gas50x baseline +// tier, where an unscheduled TIPTRC21FeeBlock keeps the legacy price. +func baselineTRC21GasPrice(cfg *ChainConfig, number *big.Int) *big.Int { + if number != nil && cfg.TIPTRC21FeeBlock != nil && number.Cmp(cfg.TIPTRC21FeeBlock) > 0 { + return new(big.Int).Set(common.TRC21GasPrice) + } + return new(big.Int).Set(common.TRC21GasPriceBefore) +} -// SetMinGasPrice50x updates minGasPrice50x based on min gas price. It is only -// called once during node startup. -func SetMinGasPrice50x(minPrice *big.Int) { - if minPrice == nil { - log.Crit("SetMinGasPrice50x can't handle nil gas price") +// BaseFeeForBlock returns the EIP-1559 base fee at number, which equals the +// chain-default gas price of the tier active there, so header validation and +// the fee schedule cannot drift apart. Before any tier has fired it falls back +// to InitialBaseFee, the value every EIP-1559 header carried before the +// schedule became fork aware. It does not check EIP1559Block, so callers +// filling a header must confirm EIP-1559 is active at number first. +func BaseFeeForBlock(cfg *ChainConfig, number *big.Int) *big.Int { + if price, ok := tierGasPrice(cfg, number); ok { + return new(big.Int).Set(price) } - minGasPrice50x = new(big.Int).Mul(minPrice, big.NewInt(50)) + return new(big.Int).SetUint64(InitialBaseFee) +} + +// BaseFeeForOpcode returns what the BASEFEE opcode reports for a block whose +// header carries no base fee, which is the London-to-EIP1559 window where XDC +// had the opcode but not the header field. It stays pinned to InitialBaseFee +// instead of resolving the gas schedule so a later tier fork cannot rewrite +// what those already executed blocks saw. CheckConfigForkOrder keeps that +// window on the Gas50x tier, whose price is InitialBaseFee, so no chain can +// schedule a higher tier while headers still lack the field. +func BaseFeeForOpcode() *big.Int { + return new(big.Int).SetUint64(InitialBaseFee) } // GetGasPriceForTRC21 returns the effective gas price for TRC21 transactions // at the given block number using the provided chain configuration. -// -// NOTE: number is not nil when called from state transition func GetGasPriceForTRC21(number *big.Int, cfg *ChainConfig) (*big.Int, error) { if cfg == nil { return nil, errors.New("chain config is nil") } + if number == nil { + return nil, errors.New("block number is nil") + } + // Stricter than GetGasFee on purpose: state transition must run against a + // fully scheduled fee config. if cfg.TIPTRC21FeeBlock == nil { return nil, errors.New("missing TIPTRC21FeeBlock in chain config") } - if cfg.Gas50xBlock != nil && number.Cmp(cfg.Gas50xBlock) >= 0 { - return new(big.Int).Set(gasPrice50x), nil + if price, ok := tierGasPrice(cfg, number); ok { + return new(big.Int).Set(price), nil } - if number.Cmp(cfg.TIPTRC21FeeBlock) > 0 { - return new(big.Int).Set(common.TRC21GasPrice), nil - } - return new(big.Int).Set(common.TRC21GasPriceBefore), nil + return baselineTRC21GasPrice(cfg, number), nil } // GetGasFee returns the effective fee for the given block height and gas usage // using the active chain configuration schedule. -// -// NOTE: caller must ensure cfg is non-nil func GetGasFee(blockNumber, gas uint64, cfg *ChainConfig) *big.Int { if cfg == nil { log.Crit("GetGasFee received nil chain config") } - fee := new(big.Int).SetUint64(gas) block := new(big.Int).SetUint64(blockNumber) - if cfg.Gas50xBlock != nil && block.Cmp(cfg.Gas50xBlock) >= 0 { - return fee.Mul(fee, gasPrice50x) + price, ok := tierGasPrice(cfg, block) + if !ok { + price = baselineTRC21GasPrice(cfg, block) } - if cfg.TIPTRC21FeeBlock != nil && block.Cmp(cfg.TIPTRC21FeeBlock) > 0 { - return fee.Mul(fee, common.TRC21GasPrice) + return new(big.Int).Mul(price, new(big.Int).SetUint64(gas)) +} + +// chainDefaultGasPrice resolves the chain-default gas price schedule at number. +func chainDefaultGasPrice(number *big.Int, cfg *ChainConfig) *big.Int { + price, ok := tierGasPrice(cfg, number) + if !ok { + price = baselineGasPrice } - return fee.Mul(fee, common.TRC21GasPriceBefore) + return new(big.Int).Set(price) } // GetGasPrice returns the chain-default gas price for the given block height. @@ -64,20 +138,14 @@ func GetGasPrice(number *big.Int, cfg *ChainConfig) *big.Int { if cfg == nil { log.Crit("GetGasPrice received nil chain config") } - if number != nil && cfg.Gas50xBlock != nil && number.Cmp(cfg.Gas50xBlock) >= 0 { - return new(big.Int).Set(gasPrice50x) - } - return new(big.Int).Set(common.TRC21GasPrice) + return chainDefaultGasPrice(number, cfg) } // GetMinGasPrice returns the chain-default minimum gas price for the given -// block height. +// block height. It is pool-local and never consensus critical. func GetMinGasPrice(number *big.Int, cfg *ChainConfig) *big.Int { if cfg == nil { log.Crit("GetMinGasPrice received nil chain config") } - if number != nil && cfg.Gas50xBlock != nil && number.Cmp(cfg.Gas50xBlock) >= 0 { - return new(big.Int).Set(minGasPrice50x) - } - return new(big.Int).Set(common.MinGasPrice) + return chainDefaultGasPrice(number, cfg) } diff --git a/params/gas_test.go b/params/gas_test.go index dfadf7124ce2..c9ea8423537b 100644 --- a/params/gas_test.go +++ b/params/gas_test.go @@ -1,16 +1,102 @@ package params import ( + "errors" "math/big" + "strings" "testing" "github.com/XinFinOrg/XDPoSChain/common" ) +// scaled multiplies base by the schedule multiplier expected for a tier. +func scaled(base *big.Int, multiplier int64) *big.Int { + return new(big.Int).Mul(base, big.NewInt(multiplier)) +} + +// TestGasPriceResultsAreIndependent guards that callers can mutate a returned +// price without corrupting the schedule for everyone else. +func TestGasPriceResultsAreIndependent(t *testing.T) { + cfg := &ChainConfig{Gas50xBlock: big.NewInt(100), Gas2500xBlock: big.NewInt(200)} + + for _, block := range []*big.Int{big.NewInt(99), big.NewInt(100), big.NewInt(200)} { + want := GetGasPrice(block, cfg) + GetGasPrice(block, cfg).SetInt64(1) + if got := GetGasPrice(block, cfg); got.Cmp(want) != 0 { + t.Fatalf("gas price at block %v corrupted by caller: have %v want %v", block, got, want) + } + } +} + +// TestGasTiersOrderedLatestFirst guards the table order tierGasPrice relies on: +// the first matching entry wins, so prices must strictly decrease from the +// latest fork to the earliest. +func TestGasTiersOrderedLatestFirst(t *testing.T) { + for i := 1; i < len(gasTiers); i++ { + if prev, cur := gasTiers[i-1].price, gasTiers[i].price; prev.Cmp(cur) <= 0 { + t.Fatalf("gasTiers[%d] price %v must exceed gasTiers[%d] price %v", i-1, prev, i, cur) + } + } +} + +// TestGasTierActivationsMatchTableOrder guards the other half of that order: a +// tier's fork must imply every later-listed tier's fork, otherwise scanning +// latest first would return a row whose predecessor has not fired yet. +func TestGasTierActivationsMatchTableOrder(t *testing.T) { + // One activation height per gasTiers entry, in the same order. + cfg := &ChainConfig{Gas2500xBlock: big.NewInt(200), Gas50xBlock: big.NewInt(100)} + forkBlocks := []int64{200, 100} + if len(forkBlocks) != len(gasTiers) { + t.Fatalf("forkBlocks covers %d tiers, want %d: schedule the new tier here too", len(forkBlocks), len(gasTiers)) + } + + probes := []*big.Int{big.NewInt(0)} + for _, block := range forkBlocks { + probes = append(probes, big.NewInt(block-1), big.NewInt(block), big.NewInt(block+1)) + } + for _, number := range probes { + newerActive := false + for i, tier := range gasTiers { + switch active := tier.activated(cfg, number); { + case active: + newerActive = true + case newerActive: + t.Fatalf("block %v: gasTiers[%d] is inactive while a newer tier is active", number, i) + } + } + } +} + +// TestGasTierGapRejectedAtStartup pins the premise of the invariant above: it +// only holds for configs that schedule every tier, and a gapped schedule is +// rejected before it can reach tierGasPrice. +func TestGasTierGapRejectedAtStartup(t *testing.T) { + cfg := &ChainConfig{ + ChainID: big.NewInt(1234), + TIPTRC21FeeBlock: big.NewInt(0), + Gas50xBlock: nil, + Gas2500xBlock: big.NewInt(200), + TRC21IssuerSMC: TestnetChainConfig.TRC21IssuerSMC, + XDCXListingSMC: TestnetChainConfig.XDCXListingSMC, + RelayerRegistrationSMC: TestnetChainConfig.RelayerRegistrationSMC, + LendingRegistrationSMC: TestnetChainConfig.LendingRegistrationSMC, + Ethash: new(EthashConfig), + } + + err := cfg.CheckConfigForkOrder() + if !errors.Is(err, ErrMissingForkSwitch) { + t.Fatalf("gapped gas schedule accepted: have %v want %v", err, ErrMissingForkSwitch) + } + if !strings.Contains(err.Error(), "Gas50xBlock") { + t.Fatalf("unexpected error string: %v", err) + } +} + func TestGetGasPriceForTRC21(t *testing.T) { cfg := &ChainConfig{ TIPTRC21FeeBlock: big.NewInt(10), Gas50xBlock: big.NewInt(20), + Gas2500xBlock: big.NewInt(30), } tests := []struct { @@ -18,10 +104,12 @@ func TestGetGasPriceForTRC21(t *testing.T) { block *big.Int want *big.Int }{ - // {name: "nil block uses pre-tip price", block: nil, want: common.TRC21GasPriceBefore}, // removed: number must not be nil {name: "activation block uses pre-tip price", block: big.NewInt(10), want: common.TRC21GasPriceBefore}, {name: "after tip block uses tip price", block: big.NewInt(11), want: common.TRC21GasPrice}, - {name: "gas50x block uses gas50x price", block: big.NewInt(20), want: gasPrice50x}, + {name: "gas50x block uses gas50x price", block: big.NewInt(20), want: scaled(new(big.Int).SetUint64(common.DefaultMinGasPrice), 50)}, + {name: "below gas2500x block stays on gas50x price", block: big.NewInt(29), want: scaled(new(big.Int).SetUint64(common.DefaultMinGasPrice), 50)}, + {name: "gas2500x block uses gas2500x price", block: big.NewInt(30), want: scaled(new(big.Int).SetUint64(common.DefaultMinGasPrice), 2500)}, + {name: "above gas2500x block stays on gas2500x price", block: big.NewInt(31), want: scaled(new(big.Int).SetUint64(common.DefaultMinGasPrice), 2500)}, } for _, tc := range tests { @@ -37,11 +125,20 @@ func TestGetGasPriceForTRC21(t *testing.T) { } } +func TestGetGasPriceForTRC21RejectsNilBlock(t *testing.T) { + cfg := &ChainConfig{TIPTRC21FeeBlock: big.NewInt(10)} + + if _, err := GetGasPriceForTRC21(nil, cfg); err == nil { + t.Fatal("expected error for nil block number") + } +} + // TestGetGasFeeUsesGas50xBlock tests get gas fee uses gas 50 x block. func TestGetGasFeeUsesGas50xBlock(t *testing.T) { cfg := &ChainConfig{ TIPTRC21FeeBlock: big.NewInt(50), Gas50xBlock: big.NewInt(100), + Gas2500xBlock: big.NewInt(200), } beforeFork := GetGasFee(99, 2, cfg) @@ -50,9 +147,14 @@ func TestGetGasFeeUsesGas50xBlock(t *testing.T) { } afterFork := GetGasFee(100, 2, cfg) - if want := new(big.Int).Mul(big.NewInt(2), gasPrice50x); afterFork.Cmp(want) != 0 { + if want := new(big.Int).Mul(big.NewInt(2), scaled(new(big.Int).SetUint64(common.DefaultMinGasPrice), 50)); afterFork.Cmp(want) != 0 { t.Fatalf("unexpected fee after gas50x fork: have %v want %v", afterFork, want) } + + afterGas2500x := GetGasFee(200, 2, cfg) + if want := new(big.Int).Mul(big.NewInt(2), scaled(new(big.Int).SetUint64(common.DefaultMinGasPrice), 2500)); afterGas2500x.Cmp(want) != 0 { + t.Fatalf("unexpected fee after gas2500x fork: have %v want %v", afterGas2500x, want) + } } // TestGetGasFeeIgnoresForkHeightsAboveUint64 tests oversized fork heights do not wrap. @@ -88,19 +190,103 @@ func TestGetGasFeeIgnoresForkHeightsAboveUint64(t *testing.T) { // TestGetGasPriceAndMinGasPriceUseGas50xBlock tests get gas price and min gas price use gas 50 x block. func TestGetGasPriceAndMinGasPriceUseGas50xBlock(t *testing.T) { + cfg := &ChainConfig{Gas50xBlock: big.NewInt(100), Gas2500xBlock: big.NewInt(200)} + + for _, tc := range []struct { + name string + block *big.Int + multiplier int64 + }{ + {name: "before gas50x fork", block: big.NewInt(99), multiplier: 1}, + {name: "at gas50x fork", block: big.NewInt(100), multiplier: 50}, + {name: "below gas2500x fork", block: big.NewInt(199), multiplier: 50}, + {name: "at gas2500x fork", block: big.NewInt(200), multiplier: 2500}, + {name: "above gas2500x fork", block: big.NewInt(201), multiplier: 2500}, + } { + t.Run(tc.name, func(t *testing.T) { + if got, want := GetGasPrice(tc.block, cfg), scaled(new(big.Int).SetUint64(common.DefaultMinGasPrice), tc.multiplier); got.Cmp(want) != 0 { + t.Fatalf("unexpected gas price: have %v want %v", got, want) + } + if got, want := GetMinGasPrice(tc.block, cfg), scaled(new(big.Int).SetUint64(common.DefaultMinGasPrice), tc.multiplier); got.Cmp(want) != 0 { + t.Fatalf("unexpected min gas price: have %v want %v", got, want) + } + }) + } +} + +// TestBaseFeeForBlockMatchesGasPriceSchedule guards the invariant that the +// EIP-1559 base fee equals the chain-default gas price of every scheduled tier, +// so adding a tier cannot silently desynchronise header validation from the fee +// schedule and the fork order of EIP1559Block stays irrelevant to pricing. +func TestBaseFeeForBlockMatchesGasPriceSchedule(t *testing.T) { + cfg := &ChainConfig{Gas50xBlock: big.NewInt(100), Gas2500xBlock: big.NewInt(200)} + + for _, block := range []*big.Int{big.NewInt(100), big.NewInt(199), big.NewInt(200), big.NewInt(201)} { + if got, want := BaseFeeForBlock(cfg, block), GetGasPrice(block, cfg); got.Cmp(want) != 0 { + t.Fatalf("base fee at block %v: have %v want %v", block, got, want) + } + } +} + +// TestBaseFeeForBlockKeepsLegacyConstantBeforeGas50x pins the baseline tier to +// InitialBaseFee instead of the chain-default gas price. It covers both the +// London-to-Gas50x window, where mainnet has already produced blocks whose +// BASEFEE reported InitialBaseFee, and any chain that enables EIP-1559 before +// scheduling Gas50x, whose headers would otherwise be priced 50x lower. +func TestBaseFeeForBlockKeepsLegacyConstantBeforeGas50x(t *testing.T) { cfg := &ChainConfig{Gas50xBlock: big.NewInt(100)} - if got := GetGasPrice(big.NewInt(99), cfg); got.Cmp(common.TRC21GasPrice) != 0 { - t.Fatalf("unexpected gas price before gas50x fork: have %v want %v", got, common.TRC21GasPrice) + for _, block := range []*big.Int{nil, big.NewInt(0), big.NewInt(99)} { + if got, want := BaseFeeForBlock(cfg, block), new(big.Int).SetUint64(InitialBaseFee); got.Cmp(want) != 0 { + t.Fatalf("unexpected pre-gas50x base fee at block %v: have %v want %v", block, got, want) + } + } +} + +// TestInitialBaseFeeMatchesGas50xTier pins InitialBaseFee to the Gas50x tier +// price. BaseFeeForBlock falls back to InitialBaseFee below the first tier; if +// the two ever diverged, the base fee would jump at Gas50xBlock and every block +// already produced in the London-to-Gas50x window would replay with a different +// state root. +func TestInitialBaseFeeMatchesGas50xTier(t *testing.T) { + cfg := &ChainConfig{Gas50xBlock: big.NewInt(0)} + + if got, want := BaseFeeForBlock(cfg, big.NewInt(0)), new(big.Int).SetUint64(InitialBaseFee); got.Cmp(want) != 0 { + t.Fatalf("gas50x base fee drifted from InitialBaseFee: have %v want %v", got, want) } - if got := GetGasPrice(big.NewInt(100), cfg); got.Cmp(gasPrice50x) != 0 { - t.Fatalf("unexpected gas price after gas50x fork: have %v want %v", got, gasPrice50x) + if got, want := new(big.Int).SetUint64(InitialBaseFee), scaled(new(big.Int).SetUint64(common.DefaultMinGasPrice), 50); got.Cmp(want) != 0 { + t.Fatalf("InitialBaseFee drifted from the gas50x tier price: have %v want %v", got, want) } - if got := GetMinGasPrice(big.NewInt(99), cfg); got.Cmp(common.MinGasPrice) != 0 { - t.Fatalf("unexpected min gas price before gas50x fork: have %v want %v", got, common.MinGasPrice) +} + +// TestGasScheduleUnaffectedByUnscheduledGas2500x is the regression guard for +// networks that have not scheduled Gas2500x yet. +func TestGasScheduleUnaffectedByUnscheduledGas2500x(t *testing.T) { + far, ok := new(big.Int).SetString("1000000000000", 10) + if !ok { + t.Fatal("failed to construct far-future fork height") } - if got := GetMinGasPrice(big.NewInt(100), cfg); got.Cmp(minGasPrice50x) != 0 { - t.Fatalf("unexpected min gas price after gas50x fork: have %v want %v", got, minGasPrice50x) + scheduled := &ChainConfig{TIPTRC21FeeBlock: big.NewInt(10), Gas50xBlock: big.NewInt(20), Gas2500xBlock: far} + unscheduled := &ChainConfig{TIPTRC21FeeBlock: big.NewInt(10), Gas50xBlock: big.NewInt(20)} + + for _, block := range []*big.Int{big.NewInt(5), big.NewInt(11), big.NewInt(20), big.NewInt(1_000_000)} { + if got, want := GetGasPrice(block, scheduled), GetGasPrice(block, unscheduled); got.Cmp(want) != 0 { + t.Fatalf("gas price drifted at block %v: have %v want %v", block, got, want) + } + if got, want := GetMinGasPrice(block, scheduled), GetMinGasPrice(block, unscheduled); got.Cmp(want) != 0 { + t.Fatalf("min gas price drifted at block %v: have %v want %v", block, got, want) + } + got, err := GetGasPriceForTRC21(block, scheduled) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + want, err := GetGasPriceForTRC21(block, unscheduled) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got.Cmp(want) != 0 { + t.Fatalf("trc21 gas price drifted at block %v: have %v want %v", block, got, want) + } } } @@ -129,9 +315,10 @@ func TestGetGasFeeUsesGetGasPriceForTRC21(t *testing.T) { cfg := &ChainConfig{ TIPTRC21FeeBlock: big.NewInt(10), Gas50xBlock: big.NewInt(20), + Gas2500xBlock: big.NewInt(30), } - for _, block := range []uint64{9, 10, 11, 20} { + for _, block := range []uint64{9, 10, 11, 20, 29, 30, 31} { fee := GetGasFee(block, 2, cfg) price, err := GetGasPriceForTRC21(new(big.Int).SetUint64(block), cfg) if err != nil { @@ -143,3 +330,17 @@ func TestGetGasFeeUsesGetGasPriceForTRC21(t *testing.T) { } } } + +// TestGasFeeResultsAreIndependent guards that GetGasFee never hands back a +// value aliasing the schedule, so a caller mutating the fee cannot corrupt it. +func TestGasFeeResultsAreIndependent(t *testing.T) { + cfg := &ChainConfig{TIPTRC21FeeBlock: big.NewInt(10), Gas50xBlock: big.NewInt(100), Gas2500xBlock: big.NewInt(200)} + + for _, block := range []uint64{9, 11, 100, 200} { + want := GetGasFee(block, 2, cfg) + GetGasFee(block, 2, cfg).SetInt64(1) + if got := GetGasFee(block, 2, cfg); got.Cmp(want) != 0 { + t.Fatalf("gas fee at block %d corrupted by caller: have %v want %v", block, got, want) + } + } +} diff --git a/tests/state_test_util.go b/tests/state_test_util.go index 54069a81d594..e35fa295a769 100644 --- a/tests/state_test_util.go +++ b/tests/state_test_util.go @@ -175,7 +175,7 @@ func (t *StateTest) RunWithGas(subtest StateSubtest, vmconfig vm.Config) (*state if baseFee == nil { // Retesteth uses `0x10` for genesis baseFee. Therefore, it defaults to // parent - 2 : 0xa as the basefee for 'this' context. - baseFee = big.NewInt(common.BaseFee.Int64()) + baseFee = new(big.Int).SetUint64(params.InitialBaseFee) } } post := t.json.Post[subtest.Fork][subtest.Index]