mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
release(v1.104.9-hotfix.4): feeConfig persist/load round-trip + mainnet C-Chain era-aware BlockGasCostStep
Two coupled fixes so the flag-day RLP replay of the mainnet C-Chain (96369) reproduces the historical block-gas-cost schedule byte-exactly (scientist byte-validated against on-chain headers): 1. PERSIST/LOAD round-trip (the fleet-wide bug): customrawdb.WriteChainConfig persisted only extra.UpgradeConfig; the base ethrawdb.WriteChainConfig marshals just the embedded geth ChainConfig (its MarshalJSON shadows the extras), so extras.FeeConfig was DROPPED — every restart reloaded BlockGasCostStep=0. Fix: persist extra.FeeConfig under a new `fee-config-` key and restore it INDEPENDENTLY of UpgradeConfig; and in SetupGenesisBlock, re-apply the genesis FeeConfig when a stored (pre-fix) config lacks it, so the whole dynamic-fee schedule is recovered on resume. Affects all chains. 2. ERA-AWARE BlockGasCostStep (mainnet C-Chain replay): the historical 200000->0 step drop was this persist bug manifesting fleet-wide at the granite/v1.36.2 restart (T_36). GetFeeConfigAt now, for chainID 96369 ONLY, returns effective BlockGasCostStep=200000 while parent.Time < 1783900524 and 0 from it onward — a RAW timestamp gate (NOT config.IsGranite: replay configs set graniteTimestamp=0, which would zero the whole chain), overriding ONLY BlockGasCostStep and preserving every other genesis feeConfig field. Both parts are required (persist alone would compute 400000 at post-T_36 zero-gap blocks whose headers are 0). TDD: core/feeconfig_era_test.go (boundary exactness, field preservation, chainID-scoping, non-mutation) PASSES; customrawdb tests pass; compiles CGO_ENABLED=0. Go-forward genesis step=0 pin is compatible (the pre-T_36 200000 is explicit, independent of persisted config). Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -366,10 +366,46 @@ func (bc *BlockChain) SubscribeAcceptedTransactionEvent(ch chan<- NewTxsEvent) e
|
||||
return bc.scope.Track(bc.txAcceptedFeed.Subscribe(ch))
|
||||
}
|
||||
|
||||
// luxMainnetCChainID is the Lux mainnet C-Chain chainID (96369). The era-aware
|
||||
// BlockGasCostStep fallback is scoped strictly to it so it can never misfire on
|
||||
// testnet/devnet/L2s.
|
||||
var luxMainnetCChainID = big.NewInt(96369)
|
||||
|
||||
// t36FeeStepDropTime is the raw block timestamp of the granite/v1.36.2 first mainnet
|
||||
// C-Chain block (1,085,252) at which the fleet-wide feeConfig persist bug dropped
|
||||
// BlockGasCostStep from 200000 to 0 across every restarted node. Replaying the C-Chain
|
||||
// byte-exactly requires the EFFECTIVE step to be 200000 while a block's PARENT timestamp
|
||||
// is before this instant, and 0 from it onward.
|
||||
//
|
||||
// This is a RAW timestamp gate, deliberately NOT config.IsGranite: replay/import configs
|
||||
// set graniteTimestamp=0, so IsGranite is true everywhere and would zero the step for the
|
||||
// whole chain. The 200000→0 drop was the persist bug at the fleet restart (which coincided
|
||||
// with granite), not granite fee semantics.
|
||||
const t36FeeStepDropTime = uint64(1783900524)
|
||||
|
||||
// mainnetEraFeeConfig returns fc unchanged for every chain except the Lux mainnet C-Chain
|
||||
// (96369). For 96369 it overrides ONLY BlockGasCostStep (200000 before t36FeeStepDropTime,
|
||||
// 0 from it onward), preserving every other feeConfig field from the (now correctly
|
||||
// persisted) genesis config. The 200000 is explicit and independent of the persisted
|
||||
// config, so pinning the go-forward genesis step=0 does not break the pre-T_36 replay.
|
||||
func mainnetEraFeeConfig(chainID *big.Int, parentTime uint64, fc commontype.FeeConfig) commontype.FeeConfig {
|
||||
if chainID == nil || chainID.Cmp(luxMainnetCChainID) != 0 {
|
||||
return fc
|
||||
}
|
||||
out := fc // struct copy; other *big.Int fields shared read-only, only BlockGasCostStep is replaced
|
||||
if parentTime >= t36FeeStepDropTime {
|
||||
out.BlockGasCostStep = big.NewInt(0)
|
||||
} else {
|
||||
out.BlockGasCostStep = big.NewInt(200_000)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// GetFeeConfigAt returns the fee configuration and the last changed block number at [parent].
|
||||
// If EVM is not activated, returns default fee config and nil block number.
|
||||
// If FeeManager is activated at [parent], returns the fee config in the precompile contract state.
|
||||
// Otherwise returns the fee config in the chain config.
|
||||
// Otherwise returns the fee config in the chain config (with the mainnet C-Chain era-aware
|
||||
// BlockGasCostStep applied — see mainnetEraFeeConfig).
|
||||
// Assumes that a valid configuration is stored when the precompile is activated.
|
||||
func (bc *BlockChain) GetFeeConfigAt(parent *types.Header) (commontype.FeeConfig, *big.Int, error) {
|
||||
config := params.GetExtra(bc.Config())
|
||||
@@ -377,7 +413,7 @@ func (bc *BlockChain) GetFeeConfigAt(parent *types.Header) (commontype.FeeConfig
|
||||
return params.DefaultFeeConfig, nil, nil
|
||||
}
|
||||
if !config.IsPrecompileEnabled(feemanager.ContractAddress, parent.Time) {
|
||||
return config.FeeConfig, common.Big0, nil
|
||||
return mainnetEraFeeConfig(bc.chainConfig.ChainID, parent.Time, config.FeeConfig), common.Big0, nil
|
||||
}
|
||||
|
||||
// try to return it from the cache
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/evm/commontype"
|
||||
)
|
||||
|
||||
// TestMainnetEraFeeConfig validates the era-aware BlockGasCostStep override that reproduces
|
||||
// the historical fleet-wide feeConfig persist-bug drop (200000->0) at T_36 on the Lux mainnet
|
||||
// C-Chain (96369), per the scientist's byte-validated spec:
|
||||
// - RAW timestamp gate (parent.Time >= 1783900524), NOT config.IsGranite.
|
||||
// - step 200000 while parent < T_36, step 0 from T_36 onward.
|
||||
// - ONLY BlockGasCostStep is overridden; every other field is preserved.
|
||||
// - scoped strictly to chainID 96369.
|
||||
func TestMainnetEraFeeConfig(t *testing.T) {
|
||||
base := commontype.FeeConfig{
|
||||
GasLimit: big.NewInt(12_000_000),
|
||||
TargetBlockRate: 2,
|
||||
MinBaseFee: big.NewInt(25_000_000_000),
|
||||
TargetGas: big.NewInt(500_000_000),
|
||||
BaseFeeChangeDenominator: big.NewInt(36),
|
||||
MinBlockGasCost: big.NewInt(0),
|
||||
MaxBlockGasCost: big.NewInt(1_000_000),
|
||||
BlockGasCostStep: big.NewInt(0), // simulates the persisted/dropped step
|
||||
}
|
||||
mainnet := big.NewInt(96369)
|
||||
|
||||
assertOtherFieldsPreserved := func(t *testing.T, got commontype.FeeConfig) {
|
||||
t.Helper()
|
||||
if got.GasLimit.Cmp(base.GasLimit) != 0 || got.MinBaseFee.Cmp(base.MinBaseFee) != 0 ||
|
||||
got.TargetGas.Cmp(base.TargetGas) != 0 || got.BaseFeeChangeDenominator.Cmp(base.BaseFeeChangeDenominator) != 0 ||
|
||||
got.MinBlockGasCost.Cmp(base.MinBlockGasCost) != 0 || got.MaxBlockGasCost.Cmp(base.MaxBlockGasCost) != 0 ||
|
||||
got.TargetBlockRate != base.TargetBlockRate {
|
||||
t.Fatalf("non-step fields must be preserved; got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// pre-T_36 on the mainnet C-Chain: step overridden to 200000, all other fields preserved.
|
||||
pre := mainnetEraFeeConfig(mainnet, t36FeeStepDropTime-1, base)
|
||||
if pre.BlockGasCostStep.Cmp(big.NewInt(200_000)) != 0 {
|
||||
t.Fatalf("pre-T_36 step = %s, want 200000", pre.BlockGasCostStep)
|
||||
}
|
||||
assertOtherFieldsPreserved(t, pre)
|
||||
|
||||
// boundary exactness: parent.Time == T_36 -> 0 (parent >= T_36); parent == T_36-1 -> 200000.
|
||||
if s := mainnetEraFeeConfig(mainnet, t36FeeStepDropTime, base).BlockGasCostStep; s.Sign() != 0 {
|
||||
t.Fatalf("parent==T_36 step = %s, want 0", s)
|
||||
}
|
||||
if s := mainnetEraFeeConfig(mainnet, t36FeeStepDropTime-1, base).BlockGasCostStep; s.Cmp(big.NewInt(200_000)) != 0 {
|
||||
t.Fatalf("parent==T_36-1 step = %s, want 200000", s)
|
||||
}
|
||||
|
||||
// post-T_36: step 0, other fields preserved.
|
||||
post := mainnetEraFeeConfig(mainnet, t36FeeStepDropTime+1000, base)
|
||||
if post.BlockGasCostStep.Sign() != 0 {
|
||||
t.Fatalf("post-T_36 step = %s, want 0", post.BlockGasCostStep)
|
||||
}
|
||||
assertOtherFieldsPreserved(t, post)
|
||||
|
||||
// any other chain is untouched, even after T_36 (must not misfire on testnet/devnet/L2s).
|
||||
if s := mainnetEraFeeConfig(big.NewInt(43114), t36FeeStepDropTime+1000, base).BlockGasCostStep; s.Cmp(base.BlockGasCostStep) != 0 {
|
||||
t.Fatalf("non-mainnet chain must be untouched, got step %s", s)
|
||||
}
|
||||
if s := mainnetEraFeeConfig(nil, t36FeeStepDropTime-1, base).BlockGasCostStep; s.Cmp(base.BlockGasCostStep) != 0 {
|
||||
t.Fatal("nil chainID must be untouched")
|
||||
}
|
||||
|
||||
// the input feeConfig must never be mutated.
|
||||
if base.BlockGasCostStep.Sign() != 0 {
|
||||
t.Fatal("input feeConfig BlockGasCostStep was mutated")
|
||||
}
|
||||
}
|
||||
@@ -293,6 +293,18 @@ func SetupGenesisBlock(
|
||||
return newcfg, stored, nil
|
||||
}
|
||||
|
||||
// Recover a dropped extras.FeeConfig from the genesis config. Pre-fix DBs persisted the
|
||||
// chain config WITHOUT extras.FeeConfig (the base ethrawdb.WriteChainConfig marshals only the
|
||||
// embedded geth ChainConfig, and the custom accessor persisted only UpgradeConfig), so on
|
||||
// resume the stored FeeConfig is empty — which reverts the whole dynamic-fee schedule
|
||||
// (BlockGasCostStep, gasLimit, minBaseFee, …) to zero. Re-apply the genesis FeeConfig when the
|
||||
// stored one is absent; the now-correctly-persisting WriteChainConfig below writes it back so
|
||||
// subsequent restarts are clean. (The mainnet C-Chain BlockGasCostStep era-override lives in
|
||||
// BlockChain.GetFeeConfigAt.)
|
||||
if storedExtra := params.GetExtra(storedcfg); storedExtra.FeeConfig.GasLimit == nil {
|
||||
storedExtra.FeeConfig = params.GetExtra(newcfg).FeeConfig
|
||||
}
|
||||
|
||||
// Notes on the following line:
|
||||
// - this is needed in coreth to handle the case where existing nodes do not
|
||||
// have the Berlin or London forks initialized by block number on disk.
|
||||
|
||||
@@ -120,16 +120,27 @@ func ReadAcceptorTip(db ethdb.KeyValueReader) (common.Hash, error) {
|
||||
// ReadChainConfig retrieves the consensus settings based on the given genesis hash.
|
||||
func ReadChainConfig(db ethdb.KeyValueReader, hash common.Hash) *params.ChainConfig {
|
||||
config := ethrawdb.ReadChainConfig(db, hash)
|
||||
if config == nil {
|
||||
return nil
|
||||
}
|
||||
extra := params.GetExtra(config)
|
||||
|
||||
upgrade, _ := db.Get(upgradeConfigKey(hash))
|
||||
if len(upgrade) == 0 {
|
||||
return config
|
||||
if upgrade, _ := db.Get(upgradeConfigKey(hash)); len(upgrade) > 0 {
|
||||
if err := json.Unmarshal(upgrade, &extra.UpgradeConfig); err != nil {
|
||||
log.Error("Invalid upgrade config JSON", "err", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extra := params.GetExtra(config)
|
||||
if err := json.Unmarshal(upgrade, &extra.UpgradeConfig); err != nil {
|
||||
log.Error("Invalid upgrade config JSON", "err", err)
|
||||
return nil
|
||||
// Restore extras.FeeConfig (the base round-trip drops it — see WriteChainConfig). Restored
|
||||
// INDEPENDENTLY of UpgradeConfig so a config with no upgrades still recovers its FeeConfig.
|
||||
// Absent for pre-fix DBs; the effective feeConfig then falls back to the chain-config value
|
||||
// (and, for the mainnet C-Chain, the era-aware BlockGasCostStep override in GetFeeConfigAt).
|
||||
if feeData, _ := db.Get(feeConfigKey(hash)); len(feeData) > 0 {
|
||||
if err := json.Unmarshal(feeData, &extra.FeeConfig); err != nil {
|
||||
log.Error("Invalid fee config JSON", "err", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
@@ -150,4 +161,15 @@ func WriteChainConfig(db ethdb.KeyValueWriter, hash common.Hash, config *params.
|
||||
if err := db.Put(upgradeConfigKey(hash), data); err != nil {
|
||||
log.Crit("Failed to store upgrade config", "err", err)
|
||||
}
|
||||
|
||||
// Persist extras.FeeConfig explicitly. The base ethrawdb.WriteChainConfig marshals only
|
||||
// the embedded geth ChainConfig (its MarshalJSON shadows the extras), so without this the
|
||||
// FeeConfig is dropped and every restart reloads BlockGasCostStep=0 — the fleet-wide bug.
|
||||
feeData, err := json.Marshal(extra.FeeConfig)
|
||||
if err != nil {
|
||||
log.Crit("Failed to JSON encode fee config", "err", err)
|
||||
}
|
||||
if err := db.Put(feeConfigKey(hash), feeData); err != nil {
|
||||
log.Crit("Failed to store fee config", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,12 @@ var (
|
||||
acceptorTipKey = []byte("AcceptorTipKey")
|
||||
// upgradeConfigPrefix prefixes upgrade bytes passed to the chain
|
||||
upgradeConfigPrefix = []byte("upgrade-config-")
|
||||
// feeConfigPrefix prefixes the persisted extras.FeeConfig. The base
|
||||
// ethrawdb.WriteChainConfig marshals only the embedded geth ChainConfig (its
|
||||
// MarshalJSON shadows the extras), and the custom accessor historically persisted
|
||||
// only UpgradeConfig — so extras.FeeConfig was dropped on the persist/load round-trip
|
||||
// (step reverted to 0 on every restart). This key persists it explicitly.
|
||||
feeConfigPrefix = []byte("fee-config-")
|
||||
)
|
||||
|
||||
// State sync progress keys and prefixes
|
||||
@@ -65,6 +71,11 @@ func upgradeConfigKey(hash common.Hash) []byte {
|
||||
return append(upgradeConfigPrefix, hash.Bytes()...)
|
||||
}
|
||||
|
||||
// feeConfigKey = feeConfigPrefix + hash
|
||||
func feeConfigKey(hash common.Hash) []byte {
|
||||
return append(feeConfigPrefix, hash.Bytes()...)
|
||||
}
|
||||
|
||||
// blockGasCostPrefix is the prefix for storing BlockGasCost data
|
||||
// blockGasCostPrefix + num (8 bytes) + hash (32 bytes) -> BlockGasCost RLP encoded
|
||||
var blockGasCostPrefix = []byte("bgc")
|
||||
|
||||
Reference in New Issue
Block a user