From 42bc4215e4f6dcdca867b9ec6f9634effb64fd17 Mon Sep 17 00:00:00 2001 From: Daniel Liu <139250065@qq.com> Date: Wed, 12 Aug 2026 08:30:10 +0800 Subject: [PATCH] feat(params,core,consensus,eth,cmd): add gas2500x fork switch Introduce an optional Gas2500xBlock fork that raises the XDC gas schedule from the Gas50x tier to the Gas2500x tier, moving the chain default gas price and the EIP-1559 base fee from 12.5 gwei to 625 gwei. Gas tier resolution is refactored into a single ordered table in params/gas.go, so a future tier only needs one more row. TRC21 gas price, gas fee and chain default gas price queries resolve the active tier instead of testing Gas50xBlock directly. Every tier, including the transaction pool floor returned by GetMinGasPrice, is scaled from the immutable common.DefaultMinGasPrice, so the cached params.SetMinGasPrice50x and the mutable common.MinGasPrice it was derived from are both removed. Tier prices are computed once at init and shared, so accessors copy before handing one out. On paper that removes the ability to raise the pool minimum gas price through --miner.gasprice, but the path was not dead: MinerGasPriceFlag is a *flags.BigFlag, and ctx.Int in urfave/cli v2 parses the flag value's decimal string (lookupInt calls strconv.ParseInt on f.Value.String()), so for any value that fits in int64 and exceeds common.DefaultMinGasPrice the guarded assignment fired and common.MinGasPrice was overridden, raising the Gas50x pool floor to miner.gasprice times 50. Removing it is therefore an operator-facing behaviour change: the floor becomes a pure function of the chain config gas schedule, no node-level knob can move it any more, and raising it now requires scheduling a tier fork. The flag still feeds the miner and the gas price oracle exactly as before. The EIP-1559 base fee becomes block-number aware: CalcBaseFee is expressed in terms of the new CalcBaseFeeForBlockNumber and the common.BaseFee constant is replaced by params.BaseFeeForBlock. Callers that price a not yet mined transaction - txpool promotion, demotion and reorg, ValidateTransactionWithState, the gas price oracle, fee history and the pending transaction RPC - now resolve the schedule at the next block height instead of at the current head. Promotion and demotion also read that head from pool.currentHead instead of pool.chain.CurrentHeader(), matching the gas limit source they already used and avoiding a race with the canonical head during reorgs. Unlike upstream, CalcBaseFee takes the block being built or verified rather than its parent, which every caller already did but which only now changes the result at a tier activation, so the doc comment spells the contract out. Two pre-existing inconsistencies are fixed along the way. The BASEFEE opcode returned a hard-coded constant regardless of the block being executed and now reports the header base fee. Where the header carries none - the London-to-EIP1559 window - it falls back to params.BaseFeeForOpcode, kept separate from BaseFeeForBlock so the opcode baseline stays pinned to InitialBaseFee rather than following the chain default gas price, and held as a precomputed uint256 because that window spans over twenty million mainnet blocks. That pinning is only safe while the window stays on the Gas50x tier, whose price is InitialBaseFee, so CheckConfigForkOrder now rejects a configuration that would let Gas2500x take effect inside it. The check is window aware rather than a plain EIP1559Block/Gas2500xBlock ordering rule: a sparse localnet genesis legitimately backfills Gas2500xBlock to 0 while London and EIP-1559 both activate later, which leaves the window empty and the schedule sound. eth_simulateV1 gated base fee synthesis on IsLondon, which made simulated blocks in that same window report a zero base fee while real blocks omit the field; it now gates on IsEIP1559. Reading the block context has one visible consequence beyond that window. eth_simulateV1 deliberately runs non-validation blocks with a zero base fee, so BASEFEE now reports 0 there instead of the old constant, matching upstream. eth_call, eth_estimateGas and the tracers are unaffected: they carry the real header base fee, and vm.Config.NoBaseFee only skips the fee check and payment in the state transition. The comments on BlockContext.BaseFee, TxContext.GasPrice and Config.NoBaseFee claiming the flag zeroes the base fee describe behaviour that no longer exists in either this fork or upstream, and are corrected. One operational consequence is left unaddressed on purpose. Admission resolves the pool floor at the next block height, so underpriced transactions are rejected from the block before a tier fires onwards, but transactions already sitting in the pending queue when it fires are not swept. They stop being mineable, since the miner filters on effective tip, yet nothing evicts them: truncatePending caps by account size rather than price and pending entries have no lifetime eviction, so they are reclaimed only once the pool fills and priced.Discard makes room for newer transactions, or on restart. Senders recover by resubmitting at the new price, and the affected set is small because admission already rejects underpriced transactions one block early, so a dedicated sweep is not worth the extra churn in the reorg path. Chain config metadata, fork ordering, backfill definitions and network configs are updated, and the chain config digest version is bumped to 2. The fork is left unscheduled on mainnet, testnet and devnet, so no existing network changes behaviour: at the Gas50x tier the derived base fee is identical to the constant it replaces. It is enabled at block 0 on localnet, which changes the localnet genesis hash and requires existing local data directories to be reset. Release notes should carry three operator facing items: - Localnet data directories must be reset, since the genesis hash changes. - BASEFEE returns 0 in eth_simulateV1 non-validation blocks instead of 12.5 gwei. - Transactions left pending across a future tier activation must be resubmitted at the new price. --- cmd/XDC/config.go | 11 - common/constants.go | 2 - consensus/misc/eip1559/eip1559.go | 19 +- consensus/misc/eip1559/eip1559_test.go | 39 +++- consensus/tests/engine_v1_tests/helper.go | 2 +- consensus/tests/engine_v2_tests/helper.go | 2 +- core/chainconfig_equal.go | 2 +- core/chainconfig_equal_test.go | 9 +- core/genesis.go | 2 +- core/genesis_test.go | 26 +++ core/state_processor_test.go | 2 +- core/txpool/legacypool/legacypool.go | 29 +-- core/txpool/legacypool/legacypool_test.go | 81 ++++++-- core/txpool/validation.go | 10 +- core/txpool/validation_denylist_test.go | 4 +- core/txpool/validation_mingasprice_test.go | 143 +++++++++++++ core/vm/eips.go | 12 +- core/vm/evm.go | 4 +- core/vm/instructions_test.go | 80 ++++++++ core/vm/interpreter.go | 2 +- eth/gasprice/feehistory.go | 5 +- eth/gasprice/gasprice.go | 5 +- internal/ethapi/api.go | 8 +- internal/ethapi/api_test.go | 4 +- internal/ethapi/simulate.go | 4 +- internal/ethapi/transaction_args_test.go | 1 + params/config.go | 23 ++- params/config_backfill.go | 11 + params/config_backfill_fields.json | 6 + params/config_backfill_generated.go | 7 + params/config_backfill_test.go | 2 + params/config_compat_test.go | 1 + params/config_forks.go | 5 + params/config_networks.go | 1 + params/config_networks_test.go | 20 ++ params/config_test.go | 172 ++++++++++++++++ params/forks/forks.go | 7 + params/forks/forks_test.go | 39 ++++ params/gas.go | 134 +++++++++--- params/gas_test.go | 225 +++++++++++++++++++-- tests/state_test_util.go | 2 +- 41 files changed, 1042 insertions(+), 121 deletions(-) create mode 100644 core/txpool/validation_mingasprice_test.go create mode 100644 params/forks/forks_test.go 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]