From b26006fb83920da2c1a9252e052e7174b0e93859 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Mon, 8 Jun 2026 03:17:29 +0000 Subject: [PATCH] feat(config): surface trust anchors; warn on a sub-activation --genesis (#107) Findings #25 and #38 (operator visibility). #25: `ion-node config` omitted MinChainWork (the eclipse/fake-chain floor markSynced enforces) and any augmenting --checkpoint, so an operator couldn't confirm the security-critical config before launch. runConfig now prints minChainWork (or "unset") and extraCheckpoint (or "none"). #38: buildConfig accepted any --genesis>=0 with no warning, so `--network mainnet --genesis 0` silently scans ~667k pointless pre-ION blocks. config.ProfileGenesisHeight exposes the per-network ION-activation default; buildConfig logs a WARN when the resolved genesis is below it. Warning (not error): a low genesis is wasteful, not incorrect; a higher-than-default genesis is fine (skips old DIDs). Test: TestProfileGenesisHeight (mainnet 667000, testnet 1764000, dev 0, unknown errors). go test -race ./... green. Co-authored-by: Liran Cohen Co-authored-by: Claude Opus 4.8 (1M context) --- cmd/ion-node/main.go | 28 ++++++++++++++++++++++++++-- docs/hardening-backlog.md | 4 ++-- internal/config/profiles.go | 11 +++++++++++ internal/config/profiles_test.go | 26 ++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 internal/config/profiles_test.go diff --git a/cmd/ion-node/main.go b/cmd/ion-node/main.go index 12035a4..1ea964c 100644 --- a/cmd/ion-node/main.go +++ b/cmd/ion-node/main.go @@ -175,7 +175,18 @@ func buildConfig(cCtx *cli.Context) (*config.Config, error) { } opts = append(opts, config.WithCheckpoint(height, hash)) } - return config.New(cCtx.String("network"), opts...) + cfg, err := config.New(cCtx.String("network"), opts...) + if err != nil { + return nil, err + } + // Sanity: warn (don't fail) if --genesis was overridden far below the network's + // ION-activation height — e.g. `--network mainnet --genesis 0` would scan ~667k + // pointless pre-ION blocks. A higher-than-default genesis is fine (skips old DIDs). + if def, derr := config.ProfileGenesisHeight(cfg.Network); derr == nil && cfg.GenesisHeight < def { + log.New(cCtx.Int("v")).Warn("genesis height is below the network's ION activation; scanning starts far too low and wastes time/bandwidth", + "network", cfg.Network, "genesisHeight", cfg.GenesisHeight, "expectedActivation", def) + } + return cfg, nil } // buildHeaderChain installs the trust anchors: the network's shipped checkpoints @@ -435,10 +446,23 @@ func runConfig(cCtx *cli.Context) error { if err != nil { return err } + // Surface the eclipse/fake-chain trust anchors so an operator can confirm them + // before launch: the min-chain-work floor (markSynced won't declare synced below + // it) and any augmenting checkpoint. The network's shipped checkpoints are always + // enforced in addition to these. + minChainWork := cfg.MinChainWork + if minChainWork == "" { + minChainWork = "unset" + } + checkpoint := "none" + if cfg.Checkpoint != nil { + checkpoint = fmt.Sprintf("%d:%s", cfg.Checkpoint.Height, cfg.Checkpoint.Hash) + } log.New(cCtx.Int("v")).Info("resolved configuration", "network", cfg.Network, "chainParams", cfg.Params.Name, "prefix", cfg.Prefix, "genesisHeight", cfg.GenesisHeight, "confirmationDepth", cfg.ConfirmationDepth, - "maxReorgDepth", cfg.MaxReorgDepth, "targetOutbound", cfg.TargetOutbound, "dataDir", cfg.DataDir) + "maxReorgDepth", cfg.MaxReorgDepth, "targetOutbound", cfg.TargetOutbound, + "minChainWork", minChainWork, "extraCheckpoint", checkpoint, "dataDir", cfg.DataDir) return nil } diff --git a/docs/hardening-backlog.md b/docs/hardening-backlog.md index 62e7423..c3cf7a9 100644 --- a/docs/hardening-backlog.md +++ b/docs/hardening-backlog.md @@ -52,7 +52,7 @@ All items below survived an independent skeptic re-reading the cited code. | 22 | ⚪ low | trivial | security | `keystore-open-no-perm-check` | ✅ done (#97) | | 23 | ⚪ low | trivial | operational | `buildpsbt-missing-mainnet-gate` | open | | 24 | ⚪ low | small | correctness | `scan-cid-shape-only-no-multihash-validation` | open | -| 25 | ⚪ low | trivial | operational | `config-command-hides-trust-anchors` | open | +| 25 | ⚪ low | trivial | operational | `config-command-hides-trust-anchors` | ✅ done (#107) | | 26 | ⚪ low | trivial | correctness | `scan-zero-and-overcount-consume-block-cap-slot` | open | | 27 | ⚪ low | medium | reliability | `observer-shutdown-blocks-on-cas-fetch` | open | | 28 | ⚪ low | small | resource | `retrypending-loads-full-set-each-pass` | open | @@ -65,7 +65,7 @@ All items below survived an independent skeptic re-reading the cited code. | 35 | ⚪ low | small | test-gap | `walletstore-no-differential-fuzz` | open | | 36 | ⚪ low | small | correctness | `headers-no-future-time-and-false-ban` | open | | 37 | ⚪ low | small | test-gap | `cmd-ion-node-zero-tests` | open | -| 38 | ⚪ low | trivial | operational | `genesis-override-no-network-sanity-check` | open | +| 38 | ⚪ low | trivial | operational | `genesis-override-no-network-sanity-check` | ✅ done (#107) | | 39 | ⚪ low | small | test-gap | `p2p-block-fetch-adversarial-untested` | open | | 40 | ⚪ low | small | test-gap | `cas-cache-content-integrity-untested` | open | diff --git a/internal/config/profiles.go b/internal/config/profiles.go index c91678a..bf12a6d 100644 --- a/internal/config/profiles.go +++ b/internal/config/profiles.go @@ -64,6 +64,17 @@ func profileFor(network string) (*Config, error) { } } +// ProfileGenesisHeight returns a network's default (ION-activation) scan-start +// height, before any --genesis override. Used to warn when an override drops the +// start far below activation (a wasteful misconfiguration). +func ProfileGenesisHeight(network string) (int32, error) { + c, err := profileFor(network) + if err != nil { + return 0, err + } + return c.GenesisHeight, nil +} + // Networks returns the supported network profile names, sorted. func Networks() []string { n := []string{"mainnet", "testnet", "signet", "regtest", "simnet"} diff --git a/internal/config/profiles_test.go b/internal/config/profiles_test.go new file mode 100644 index 0000000..91569f9 --- /dev/null +++ b/internal/config/profiles_test.go @@ -0,0 +1,26 @@ +package config + +import "testing" + +func TestProfileGenesisHeight(t *testing.T) { + cases := map[string]int32{ + "mainnet": 667000, + "testnet": 1764000, + "signet": 0, + "regtest": 0, + "simnet": 0, + } + for net, want := range cases { + got, err := ProfileGenesisHeight(net) + if err != nil { + t.Errorf("ProfileGenesisHeight(%q): %v", net, err) + continue + } + if got != want { + t.Errorf("ProfileGenesisHeight(%q) = %d, want %d", net, got, want) + } + } + if _, err := ProfileGenesisHeight("nope"); err == nil { + t.Error("ProfileGenesisHeight(unknown) should error") + } +}