Skip to content

Commit 0cd48b0

Browse files
committed
feat(core,cmd,eth,docs): handle chain config mismatch by cli flag
Replace unconditional rewind-on-mismatch with an explicit chain-config mismatch policy exposed via --chain-config-mismatch-policy and ethconfig.Config.ChainConfigMismatchPolicy. Supported policies: - exit (default): fail fast without mutating the database. - rewind-and-update: rewind to compatErr.RewindTo and persist the resolved config. - update-config-only: persist the resolved config without rewinding. - ignore-mismatch: keep the in-memory config and skip database writes. Writable opens preserve the selected recovery mode, while readonly opens reject policies that would require rewinds or writes. The CLI resolves the policy from config or flag, threads it through MakeChain, and documents the new operator-facing startup behavior.
1 parent f2d6648 commit 0cd48b0

14 files changed

Lines changed: 541 additions & 78 deletions

cmd/XDC/chaincmd.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ func importChain(ctx *cli.Context) error {
219219
// Start metrics export if enabled
220220
utils.SetupMetrics(&cfg.Metrics)
221221

222-
chain, db := utils.MakeChain(ctx, stack, false)
222+
chain, db := utils.MakeChain(ctx, stack, false, cfg.Eth.ChainConfigMismatchPolicy)
223223
defer db.Close()
224224

225225
// Start periodically gathering memory profiles
@@ -291,10 +291,10 @@ func exportChain(ctx *cli.Context) error {
291291
utils.Fatalf("This command requires an argument.")
292292
}
293293

294-
stack, _ := makeConfigNode(ctx)
294+
stack, cfg := makeConfigNode(ctx)
295295
defer stack.Close()
296296

297-
chain, db := utils.MakeChain(ctx, stack, true)
297+
chain, db := utils.MakeChain(ctx, stack, true, cfg.Eth.ChainConfigMismatchPolicy)
298298
defer db.Close()
299299
start := time.Now()
300300

cmd/XDC/genesis_startup_test.go

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"testing"
1313
"time"
1414

15+
cmdutils "github.com/XinFinOrg/XDPoSChain/cmd/utils"
1516
"github.com/XinFinOrg/XDPoSChain/common"
1617
"github.com/XinFinOrg/XDPoSChain/core"
1718
"github.com/XinFinOrg/XDPoSChain/core/rawdb"
@@ -117,13 +118,17 @@ func assertCommandSucceeds(t *testing.T, cmd *testXDC) {
117118
}
118119
}
119120

120-
func startTestnetConsole(t *testing.T, datadir string) {
121+
func startTestnetConsole(t *testing.T, datadir string, extraArgs ...string) {
121122
t.Helper()
122123

123-
startupCmd := runXDC(t,
124+
args := []string{
124125
"--testnet", "console", "--port", "0", "--maxpeers", "0", "--nodiscover", "--nat", "none", "--ipcdisable",
125-
"--datadir", datadir, "--exec", "2+2",
126-
)
126+
"--datadir", datadir,
127+
}
128+
args = append(args, extraArgs...)
129+
args = append(args, "--exec", "2+2")
130+
131+
startupCmd := runXDC(t, args...)
127132
assertCommandSucceeds(t, startupCmd)
128133
}
129134

@@ -855,7 +860,12 @@ func TestOfflineExportFailsReadonlyConfigRewindWithoutMutation(t *testing.T) {
855860
injectedHeadHash := injectCanonicalHeadBlock(t, datadir, genesisHash, 101)
856861

857862
exportCmd := runXDC(t, "--datadir", datadir, "--testnet", "export", exportFile)
858-
assertCommandFailsWithChainConfigError(t, exportCmd, "Can't open blockchain in readonly mode: the local chain configuration requires rewind. Use the correct --networkid/--datadir combination, or reopen the database in writable mode so the chain can rewind, then retry.")
863+
assertCommandFailsWithChainConfigErrors(
864+
t,
865+
exportCmd,
866+
"Can't open blockchain: mismatching",
867+
cmdutils.ChainConfigMismatchPolicyExitHint,
868+
)
859869
if _, err := os.Stat(exportFile); err == nil {
860870
t.Fatalf("expected export to fail without creating %s", exportFile)
861871
} else if !os.IsNotExist(err) {
@@ -929,7 +939,12 @@ func TestOfflineExportFailsReadonlyConfigRewindToZeroWithoutMutation(t *testing.
929939
injectedHeadHash := injectCanonicalHeadBlock(t, datadir, genesisHash, 1)
930940

931941
exportCmd := runXDC(t, "--datadir", datadir, "--testnet", "export", exportFile)
932-
assertCommandFailsWithChainConfigError(t, exportCmd, "Can't open blockchain in readonly mode: the local chain configuration requires rewind. Use the correct --networkid/--datadir combination, or reopen the database in writable mode so the chain can rewind, then retry.")
942+
assertCommandFailsWithChainConfigErrors(
943+
t,
944+
exportCmd,
945+
"Can't open blockchain: mismatching",
946+
cmdutils.ChainConfigMismatchPolicyExitHint,
947+
)
933948
if _, err := os.Stat(exportFile); err == nil {
934949
t.Fatalf("expected export to fail without creating %s", exportFile)
935950
} else if !os.IsNotExist(err) {
@@ -967,7 +982,7 @@ func TestStartupRewindsStoredBuiltInHistoricalForkDrift(t *testing.T) {
967982
genesisHash := overwriteStoredBerlinBlock(t, datadir, big.NewInt(100))
968983
injectedHeadHash := injectCanonicalHeadBlock(t, datadir, genesisHash, 101)
969984

970-
startTestnetConsole(t, datadir)
985+
startTestnetConsole(t, datadir, "--chain-config-mismatch-policy", "rewind-and-update")
971986

972987
path := filepath.Join(datadir, "XDC", "chaindata")
973988
db := openTestChainDB(t, path)
@@ -993,12 +1008,12 @@ func TestStartupRewindsStoredBuiltInHistoricalForkDrift(t *testing.T) {
9931008
func TestStartupRewindsStoredBuiltInConfigDriftToZero(t *testing.T) {
9941009
datadir := t.TempDir()
9951010

996-
startTestnetConsole(t, datadir)
1011+
startTestnetConsole(t, datadir, "--chain-config-mismatch-policy", "rewind-and-update")
9971012

9981013
genesisHash := overwriteStoredEIP150Block(t, datadir, big.NewInt(1))
9991014
injectedHeadHash := injectCanonicalHeadBlock(t, datadir, genesisHash, 1)
10001015

1001-
startTestnetConsole(t, datadir)
1016+
startTestnetConsole(t, datadir, "--chain-config-mismatch-policy", "rewind-and-update")
10021017

10031018
path := filepath.Join(datadir, "XDC", "chaindata")
10041019
db := openTestChainDB(t, path)

cmd/XDC/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ var (
122122
utils.VMTraceJsonConfigFlag,
123123
utils.NetworkIdFlag,
124124
utils.AllowBuiltInConfigOverrideFlag,
125+
utils.ChainConfigMismatchPolicyFlag,
125126
utils.HTTPCORSDomainFlag,
126127
utils.AuthListenFlag,
127128
utils.AuthPortFlag,

cmd/utils/cmd.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ import (
4343

4444
const (
4545
importBatchSize = 2500
46+
47+
ChainConfigMismatchPolicyExitHint = "Hint: Use --chain-config-mismatch-policy to recover; Or restart with a matching XDC binary and the same network/genesis settings."
4648
)
4749

4850
// Fatalf formats a message to standard error and exits the program.
@@ -72,6 +74,22 @@ func FormatChainConfigError(err error) string {
7274
return ""
7375
}
7476
message := err.Error()
77+
if errors.Is(err, core.ErrConfigMismatchPolicyExit) {
78+
exitText := core.ErrConfigMismatchPolicyExit.Error()
79+
guidance := ChainConfigMismatchPolicyExitHint
80+
prefix := exitText + ": "
81+
if after, ok := strings.CutPrefix(message, prefix); ok {
82+
details := strings.TrimSpace(after)
83+
if details == "" {
84+
return guidance
85+
}
86+
return details + ".\n" + guidance
87+
}
88+
if message == exitText {
89+
return guidance
90+
}
91+
return message + ". " + ChainConfigMismatchPolicyExitHint
92+
}
7593
if !errors.Is(err, params.ErrMissingForkSwitch) {
7694
return message
7795
}

cmd/utils/flags.go

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@ var (
131131
Usage: "Allow same-hash custom overrides on built-in IDs to use custom chain config",
132132
Category: flags.EthCategory,
133133
}
134+
ChainConfigMismatchPolicyFlag = &cli.StringFlag{
135+
Name: "chain-config-mismatch-policy",
136+
Usage: "Startup policy when chain config mismatches stored config: exit|rewind-and-update|update-config-only|ignore-mismatch (warning: update-config-only/ignore-mismatch may cause state/consensus divergence; expert use only)",
137+
Value: core.DefaultChainConfigMismatchPolicy.String(),
138+
Category: flags.EthCategory,
139+
}
134140

135141
// Dev mode
136142
DeveloperFlag = &cli.BoolFlag{
@@ -1520,6 +1526,7 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
15201526
setMiner(ctx, &cfg.Miner)
15211527
setLes(ctx, cfg)
15221528
cfg.AllowBuiltInCustomRecovery = ctx.Bool(AllowBuiltInConfigOverrideFlag.Name)
1529+
cfg.ChainConfigMismatchPolicy = resolveChainConfigMismatchPolicyOrFatal(ctx, cfg.ChainConfigMismatchPolicy)
15231530

15241531
// Cap the cache allowance and tune the garbage collector
15251532
mem, err := gopsutil.VirtualMemory()
@@ -1850,14 +1857,18 @@ func formatBlockChainOpenError(err error, readonly bool) string {
18501857
return fmt.Sprintf("Can't create BlockChain: %v", err)
18511858
}
18521859
switch {
1860+
case errors.Is(err, core.ErrConfigMismatchPolicyExit):
1861+
return "Can't open blockchain: " + FormatChainConfigError(err)
18531862
case errors.Is(err, core.ErrReadOnlyGenesisStateRecovery):
18541863
return "Can't open blockchain in readonly mode: genesis state is missing and requires recovery. Reopen the database in writable mode to recover the missing genesis state, then retry."
18551864
case errors.Is(err, core.ErrReadOnlyHeadStateRepair):
18561865
return "Can't open blockchain in readonly mode: head state is missing and requires repair. Reopen the database in writable mode to repair the missing head state, then retry."
18571866
case errors.Is(err, core.ErrReadOnlyBadHashRewind):
18581867
return "Can't open blockchain in readonly mode: the local chain contains a denylisted hash and requires rewind. Reopen the database in writable mode so the chain can rewind past the denylisted hash, then retry."
18591868
case errors.Is(err, core.ErrReadOnlyConfigRewind):
1860-
return "Can't open blockchain in readonly mode: the local chain configuration requires rewind. Use the correct --networkid/--datadir combination, or reopen the database in writable mode so the chain can rewind, then retry."
1869+
return "Can't open blockchain in readonly mode: the selected chain-config mismatch policy requires rewind. Reopen in writable mode, or use --chain-config-mismatch-policy=ignore-mismatch to avoid rewind in readonly mode."
1870+
case errors.Is(err, core.ErrReadOnlyConfigUpdate):
1871+
return "Can't open blockchain in readonly mode: the selected chain-config mismatch policy requires writing chain config. Reopen in writable mode, or use --chain-config-mismatch-policy=ignore-mismatch in readonly mode."
18611872
default:
18621873
return fmt.Sprintf("Can't create BlockChain: %v", err)
18631874
}
@@ -1866,14 +1877,15 @@ func formatBlockChainOpenError(err error, readonly bool) string {
18661877
var makeChainFatalf = Fatalf
18671878

18681879
// MakeChain creates a chain manager from set command line flags.
1869-
func MakeChain(ctx *cli.Context, stack *node.Node, readonly bool) (*core.BlockChain, ethdb.Database) {
1880+
func MakeChain(ctx *cli.Context, stack *node.Node, readonly bool, configuredCompatPolicy string) (*core.BlockChain, ethdb.Database) {
18701881
var (
1871-
gspec = MakeGenesis(ctx)
1872-
chainDb = MakeChainDatabase(ctx, stack, readonly)
1873-
config *params.ChainConfig
1874-
ghash common.Hash
1875-
compatErr *params.ConfigCompatError
1876-
err error
1882+
gspec = MakeGenesis(ctx)
1883+
chainDb = MakeChainDatabase(ctx, stack, readonly)
1884+
config *params.ChainConfig
1885+
ghash common.Hash
1886+
compatErr *params.ConfigCompatError
1887+
compatPolicy = core.ChainConfigMismatchPolicy(resolveChainConfigMismatchPolicyOrFatal(ctx, configuredCompatPolicy))
1888+
err error
18771889
)
18781890
if readonly {
18791891
// Readonly startup still needs compatibility metadata so chain open can
@@ -1933,9 +1945,9 @@ func MakeChain(ctx *cli.Context, stack *node.Node, readonly bool) (*core.BlockCh
19331945
// Disable transaction indexing/unindexing by default.
19341946
var chain *core.BlockChain
19351947
if readonly {
1936-
chain, err = core.NewBlockChainReadOnlyResolved(chainDb, cache, gspec, engine, vmcfg, config, ghash, compatErr)
1948+
chain, err = core.NewBlockChainReadOnlyResolved(chainDb, cache, gspec, engine, vmcfg, config, ghash, compatErr, compatPolicy)
19371949
} else {
1938-
chain, err = core.NewBlockChainResolved(chainDb, cache, gspec, engine, vmcfg, config, ghash, compatErr)
1950+
chain, err = core.NewBlockChainResolved(chainDb, cache, gspec, engine, vmcfg, config, ghash, compatErr, compatPolicy)
19391951
}
19401952
if err != nil {
19411953
makeChainFatalf("%s", formatBlockChainOpenError(err, readonly))
@@ -1944,6 +1956,29 @@ func MakeChain(ctx *cli.Context, stack *node.Node, readonly bool) (*core.BlockCh
19441956
return chain, chainDb
19451957
}
19461958

1959+
func resolveChainConfigMismatchPolicy(ctx *cli.Context, configured string) (string, error) {
1960+
raw := configured
1961+
errPrefix := "invalid ChainConfigMismatchPolicy in config"
1962+
if ctx.IsSet(ChainConfigMismatchPolicyFlag.Name) {
1963+
raw = ctx.String(ChainConfigMismatchPolicyFlag.Name)
1964+
errPrefix = fmt.Sprintf("invalid --%s flag", ChainConfigMismatchPolicyFlag.Name)
1965+
}
1966+
policy, err := core.ParseChainConfigMismatchPolicy(raw)
1967+
if err != nil {
1968+
return "", fmt.Errorf("%s: %w", errPrefix, err)
1969+
}
1970+
log.Info("Resolved chain config mismatch policy", "value", policy.String())
1971+
return policy.String(), nil
1972+
}
1973+
1974+
func resolveChainConfigMismatchPolicyOrFatal(ctx *cli.Context, configured string) string {
1975+
policy, err := resolveChainConfigMismatchPolicy(ctx, configured)
1976+
if err != nil {
1977+
makeChainFatalf("%v", err)
1978+
}
1979+
return policy
1980+
}
1981+
19471982
// MakeConsolePreloads retrieves the absolute paths for the console JavaScript
19481983
// scripts to preload before starting.
19491984
func MakeConsolePreloads(ctx *cli.Context) []string {

cmd/utils/flags_test.go

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"os"
2727
"path/filepath"
2828
"reflect"
29+
"strings"
2930
"testing"
3031

3132
"github.com/XinFinOrg/XDPoSChain/consensus/ethash"
@@ -195,10 +196,11 @@ func TestMakeChainWriteModePassesCompatRewindToCore(t *testing.T) {
195196
}
196197

197198
ctx := newMakeChainTestCLIContext(t, map[string]string{
198-
TestnetFlag.Name: "true",
199-
GCModeFlag.Name: "full",
199+
TestnetFlag.Name: "true",
200+
GCModeFlag.Name: "full",
201+
ChainConfigMismatchPolicyFlag.Name: core.MismatchRewindAndUpdate.String(),
200202
})
201-
chain, reopenedDb := MakeChain(ctx, stack, false)
203+
chain, reopenedDb := MakeChain(ctx, stack, false, "")
202204
defer chain.Stop()
203205
defer reopenedDb.Close()
204206

@@ -269,7 +271,7 @@ func TestMakeChainReadOnlyModeSurfacesCompatRewind(t *testing.T) {
269271
t.Fatal("expected compatibility error")
270272
}
271273

272-
chain, err := core.NewBlockChainReadOnlyResolved(readonlyDb, nil, genesis, ethash.NewFaker(), vm.Config{}, config, ghash, compatErr)
274+
chain, err := core.NewBlockChainReadOnlyResolved(readonlyDb, nil, genesis, ethash.NewFaker(), vm.Config{}, config, ghash, compatErr, core.MismatchRewindAndUpdate)
273275
if chain != nil {
274276
chain.Stop()
275277
t.Fatal("expected readonly blockchain open to fail")
@@ -317,8 +319,9 @@ func TestMakeChainReadOnlyModeFormatsCompatRewindForOperators(t *testing.T) {
317319
}
318320

319321
ctx := newMakeChainTestCLIContext(t, map[string]string{
320-
TestnetFlag.Name: "true",
321-
GCModeFlag.Name: "full",
322+
TestnetFlag.Name: "true",
323+
GCModeFlag.Name: "full",
324+
ChainConfigMismatchPolicyFlag.Name: core.MismatchRewindAndUpdate.String(),
322325
})
323326

324327
const sentinel = "fatal called"
@@ -334,13 +337,13 @@ func TestMakeChainReadOnlyModeFormatsCompatRewindForOperators(t *testing.T) {
334337
if recovered := recover(); recovered != sentinel {
335338
t.Fatalf("expected sentinel panic, have %v", recovered)
336339
}
337-
want := "Can't open blockchain in readonly mode: the local chain configuration requires rewind. Use the correct --networkid/--datadir combination, or reopen the database in writable mode so the chain can rewind, then retry."
340+
want := "Can't open blockchain in readonly mode: the selected chain-config mismatch policy requires rewind. Reopen in writable mode, or use --chain-config-mismatch-policy=ignore-mismatch to avoid rewind in readonly mode."
338341
if got != want {
339342
t.Fatalf("unexpected fatal message: have %q want %q", got, want)
340343
}
341344
}()
342345

343-
MakeChain(ctx, stack, true)
346+
MakeChain(ctx, stack, true, "")
344347
t.Fatal("expected MakeChain to terminate via fatal hook")
345348
}
346349

@@ -371,7 +374,17 @@ func TestFormatBlockChainOpenErrorReadOnly(t *testing.T) {
371374
{
372375
name: "config rewind",
373376
err: core.ErrReadOnlyConfigRewind,
374-
want: "Can't open blockchain in readonly mode: the local chain configuration requires rewind. Use the correct --networkid/--datadir combination, or reopen the database in writable mode so the chain can rewind, then retry.",
377+
want: "Can't open blockchain in readonly mode: the selected chain-config mismatch policy requires rewind. Reopen in writable mode, or use --chain-config-mismatch-policy=ignore-mismatch to avoid rewind in readonly mode.",
378+
},
379+
{
380+
name: "config update",
381+
err: core.ErrReadOnlyConfigUpdate,
382+
want: "Can't open blockchain in readonly mode: the selected chain-config mismatch policy requires writing chain config. Reopen in writable mode, or use --chain-config-mismatch-policy=ignore-mismatch in readonly mode.",
383+
},
384+
{
385+
name: "config exit",
386+
err: core.ErrConfigMismatchPolicyExit,
387+
want: "Can't open blockchain: " + ChainConfigMismatchPolicyExitHint,
375388
},
376389
}
377390

@@ -394,12 +407,65 @@ func TestFormatBlockChainOpenErrorReadOnly(t *testing.T) {
394407
}
395408
}
396409

410+
func TestResolveChainConfigMismatchPolicyHonorsConfiguredWhenFlagNotSet(t *testing.T) {
411+
t.Parallel()
412+
413+
set := flag.NewFlagSet("resolve-policy-test", flag.ContinueOnError)
414+
set.String(ChainConfigMismatchPolicyFlag.Name, core.DefaultChainConfigMismatchPolicy.String(), "")
415+
ctx := cli.NewContext(cli.NewApp(), set, nil)
416+
417+
got, err := resolveChainConfigMismatchPolicy(ctx, core.MismatchIgnoreMismatch.String())
418+
if err != nil {
419+
t.Fatalf("unexpected error: %v", err)
420+
}
421+
if got != core.MismatchIgnoreMismatch.String() {
422+
t.Fatalf("unexpected policy: have %q want %q", got, core.MismatchIgnoreMismatch.String())
423+
}
424+
}
425+
426+
func TestResolveChainConfigMismatchPolicyFlagOverridesConfigured(t *testing.T) {
427+
t.Parallel()
428+
429+
set := flag.NewFlagSet("resolve-policy-override-test", flag.ContinueOnError)
430+
set.String(ChainConfigMismatchPolicyFlag.Name, core.DefaultChainConfigMismatchPolicy.String(), "")
431+
if err := set.Set(ChainConfigMismatchPolicyFlag.Name, core.MismatchUpdateConfigOnly.String()); err != nil {
432+
t.Fatalf("failed to set policy flag: %v", err)
433+
}
434+
ctx := cli.NewContext(cli.NewApp(), set, nil)
435+
436+
got, err := resolveChainConfigMismatchPolicy(ctx, core.MismatchIgnoreMismatch.String())
437+
if err != nil {
438+
t.Fatalf("unexpected error: %v", err)
439+
}
440+
if got != core.MismatchUpdateConfigOnly.String() {
441+
t.Fatalf("unexpected policy: have %q want %q", got, core.MismatchUpdateConfigOnly.String())
442+
}
443+
}
444+
445+
func TestResolveChainConfigMismatchPolicyRejectsInvalidConfiguredValue(t *testing.T) {
446+
t.Parallel()
447+
448+
set := flag.NewFlagSet("resolve-policy-invalid-test", flag.ContinueOnError)
449+
set.String(ChainConfigMismatchPolicyFlag.Name, core.DefaultChainConfigMismatchPolicy.String(), "")
450+
ctx := cli.NewContext(cli.NewApp(), set, nil)
451+
452+
_, err := resolveChainConfigMismatchPolicy(ctx, "not-a-policy")
453+
if err == nil {
454+
t.Fatal("expected error for invalid configured policy")
455+
}
456+
const want = "invalid ChainConfigMismatchPolicy in config"
457+
if !strings.HasPrefix(err.Error(), want) {
458+
t.Fatalf("unexpected error: %v", err)
459+
}
460+
}
461+
397462
// newMakeChainTestCLIContext builds a minimal CLI context for MakeChain tests.
398463
func newMakeChainTestCLIContext(t *testing.T, values map[string]string) *cli.Context {
399464
t.Helper()
400465
set := flag.NewFlagSet("make-chain-test", flag.ContinueOnError)
401466
set.Bool(TestnetFlag.Name, false, "")
402467
set.Bool(AllowBuiltInConfigOverrideFlag.Name, false, "")
468+
set.String(ChainConfigMismatchPolicyFlag.Name, core.DefaultChainConfigMismatchPolicy.String(), "")
403469
set.String(GCModeFlag.Name, "full", "")
404470
for name, value := range values {
405471
if err := set.Set(name, value); err != nil {

0 commit comments

Comments
 (0)