mirror of
https://github.com/luxfi/explorer.git
synced 2026-09-24 00:25:12 +00:00
honour chains[].enabled — it was declared and read nowhere
`ChainConfig.Enabled` existed on the struct, was written in every chains.yaml
we ship, and was consulted by nothing. Only `Graph.Enabled` and the subgraph
flag were ever read (supervisor.go). So `enabled: false` registered the chain,
spawned its indexer and polled its RPC exactly like `enabled: true` — a switch
that looked like a switch and was wired to nothing.
Found by using it. lux-mainnet's config lists spc, which is a sovereign L1 and
has never been a chain on that network. Setting `enabled: false` and rolling
the config changed nothing: the pod still logged
[registry] added chain spc (…/v1/bc/spc/rpc) source=config
[spc/amm] [indexer] starting
[evm] Failed to get latest block: http status 404
at 1,800 errors an hour, against a chain that was already switched off.
Honoured in ChainRegistry.Add, which is the single door — config, env, mDNS
and the admin API all arrive there — so this is the whole of what the field
means, in one place. A disabled chain is not registered, so it does not index,
mounts no routes, and does not reach the SPA's chain list (which reads
registry.List()).
Enabled is now *bool because absent must not mean off. Every config written
while the field was inert says `enabled: true` out of convention; a plain bool
would read a future config that simply omits the key as false and make that
chain vanish with no error and no log. Nil is no opinion; only an explicit
`enabled: false` disables.
Tests: registration gated in all three states (true / false / absent), a
disabled chain proven not to reach the supervisor or the chain list, and
LoadConfig proven to decode into the pointer — a wrong yaml tag would leave it
nil and silently re-enable everything it disables. Verified by defeating the
check: the false and supervisor cases fail, the absent case still passes.
Also: the Dockerfile now runs vet + tests in the builder stage. This repo has
exactly one workflow, release.yml, which builds on a v* tag; nothing ran the
test files at all. Putting them where cgo and sqlite-dev already exist costs
no new job, no new runner dependency, and means a release that fails them
never becomes an image.
Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -37,6 +37,13 @@ RUN --mount=type=secret,id=ghtok,required=false \
|
||||
go mod download
|
||||
|
||||
COPY . .
|
||||
# Tests run here, in the one place that already has cgo + sqlite-dev, so a
|
||||
# release that fails them never becomes an image. Nothing else in this repo
|
||||
# runs them: .hanzo/workflows/release.yml builds on a v* tag and there is no
|
||||
# other workflow, so a test file added to this repo was, until now, never
|
||||
# executed by anything but a developer's laptop.
|
||||
RUN CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go vet ./... && \
|
||||
CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go test ./...
|
||||
RUN CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" \
|
||||
go build -trimpath \
|
||||
-ldflags="-s -w -X main.version=${VERSION}" \
|
||||
|
||||
@@ -41,7 +41,16 @@ type ChainConfig struct {
|
||||
WS string `json:"ws" yaml:"ws"`
|
||||
CoinSymbol string `json:"coin" yaml:"coin"`
|
||||
PoolManager string `json:"pool_manager" yaml:"pool_manager"` // DEX settlement precompile (0x9999); empty => indexer default
|
||||
Enabled bool `json:"enabled" yaml:"enabled"`
|
||||
// Enabled gates the chain entirely: a disabled chain is never registered,
|
||||
// so it is not indexed, mounts no routes, and does not reach the SPA's
|
||||
// chain list. See ChainRegistry.Add, the single place it is honoured.
|
||||
//
|
||||
// A pointer, so absent and false are distinguishable. Absent means ON —
|
||||
// every config predating this field wrote `enabled: true` by convention
|
||||
// while the field was inert, and a plain bool would read a future config
|
||||
// that simply omits the key as "off" and make the chain vanish with no
|
||||
// error. Nil is no opinion; only an explicit `enabled: false` disables.
|
||||
Enabled *bool `json:"enabled,omitempty" yaml:"enabled,omitempty"`
|
||||
Default bool `json:"default" yaml:"default"`
|
||||
Source string `json:"source" yaml:"-"` // config, env, mdns, admin
|
||||
|
||||
@@ -100,6 +109,10 @@ type Network struct {
|
||||
ChainID int64 `yaml:"chain_id" json:"chainId"`
|
||||
}
|
||||
|
||||
// On reports whether the chain should run. Absent means on; only an explicit
|
||||
// `enabled: false` turns a chain off.
|
||||
func (c ChainConfig) On() bool { return c.Enabled == nil || *c.Enabled }
|
||||
|
||||
// slugPattern restricts chain slugs to lowercase alphanumeric + hyphen so they
|
||||
// remain safe in URL routes (/v1/indexer/{slug}/) and filesystem paths.
|
||||
var slugPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,63}$`)
|
||||
|
||||
+13
@@ -55,6 +55,19 @@ func (r *ChainRegistry) Add(cfg ChainConfig) error {
|
||||
if cfg.Type == "" {
|
||||
cfg.Type = "evm"
|
||||
}
|
||||
// A disabled chain is not registered at all. Every path in — config, env,
|
||||
// mDNS, admin — lands here, so this is the whole of what `enabled` means.
|
||||
//
|
||||
// It used to mean nothing. The field was declared on ChainConfig and read
|
||||
// nowhere, so `enabled: false` registered the chain, spawned its indexer
|
||||
// and polled its RPC exactly like `enabled: true`. lux-mainnet carried an
|
||||
// spc entry pointing at a chain that does not exist on that network; the
|
||||
// indexer polled it forever and logged 1,800 errors an hour against a
|
||||
// switch that was already off.
|
||||
if !cfg.On() {
|
||||
log.Printf("[registry] chain %s disabled, not registered", cfg.Slug)
|
||||
return nil
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
if existing, ok := r.chains[cfg.Slug]; ok {
|
||||
|
||||
@@ -413,3 +413,86 @@ func TestHTTPIntegration(t *testing.T) {
|
||||
t.Fatalf("expected 0 chains after delete")
|
||||
}
|
||||
}
|
||||
|
||||
// TestChainEnabledGatesRegistration pins what `enabled` means. It meant nothing
|
||||
// before: the field was declared on ChainConfig and read nowhere, so a chain
|
||||
// switched off in config was registered, indexed and polled exactly like one
|
||||
// switched on. Each case below fails independently if the check is removed or
|
||||
// if the absent-means-on default is dropped.
|
||||
func TestChainEnabledGatesRegistration(t *testing.T) {
|
||||
on, off := true, false
|
||||
base := ChainConfig{RPC: "http://localhost:9650/v1/bc/C/rpc", Type: "evm", Source: "config"}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
enabled *bool
|
||||
register bool
|
||||
}{
|
||||
{"explicit false does not register", &off, false},
|
||||
{"explicit true registers", &on, true},
|
||||
// The reason Enabled is a pointer. Every chains.yaml written while the
|
||||
// field was inert says `enabled: true` by convention, but a plain bool
|
||||
// would read a config that simply omits the key as "off" and drop the
|
||||
// chain silently — no error, no log, just a missing chain.
|
||||
{"absent registers", nil, true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := NewChainRegistry()
|
||||
c := base
|
||||
c.Slug, c.Enabled = "cchain", tc.enabled
|
||||
if err := r.Add(c); err != nil {
|
||||
t.Fatalf("Add: %v", err)
|
||||
}
|
||||
if got := r.Count() == 1; got != tc.register {
|
||||
t.Fatalf("registered=%v, want %v", got, tc.register)
|
||||
}
|
||||
if _, ok := r.Get("cchain"); ok != tc.register {
|
||||
t.Fatalf("Get ok=%v, want %v", ok, tc.register)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDisabledChainNeverStartsIndexer is the failure this fixes, stated as the
|
||||
// symptom: a disabled chain must not reach the supervisor, because reaching it
|
||||
// is what spawned the indexer goroutine that polled a nonexistent RPC forever.
|
||||
func TestDisabledChainNeverStartsIndexer(t *testing.T) {
|
||||
off := false
|
||||
r := NewChainRegistry()
|
||||
r.AttachSupervisor(nil) // a real supervisor would spawn goroutines; nil panics if reached
|
||||
if err := r.Add(ChainConfig{
|
||||
Slug: "spc", RPC: "http://localhost:9650/v1/bc/spc/rpc", Type: "evm",
|
||||
Source: "config", Enabled: &off,
|
||||
}); err != nil {
|
||||
t.Fatalf("Add: %v", err)
|
||||
}
|
||||
if r.Count() != 0 {
|
||||
t.Fatalf("disabled chain reached the registry: count=%d", r.Count())
|
||||
}
|
||||
if len(r.List()) != 0 {
|
||||
t.Fatalf("disabled chain reached the SPA chain list: %v", r.List())
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadConfigKeepsEnabled proves the YAML actually decodes into the pointer —
|
||||
// a wrong tag would leave it nil and silently re-enable every disabled chain.
|
||||
func TestLoadConfigKeepsEnabled(t *testing.T) {
|
||||
p := filepath.Join(t.TempDir(), "chains.yaml")
|
||||
if err := os.WriteFile(p, []byte(
|
||||
"chains:\n"+
|
||||
" - slug: cchain\n rpc: http://x/rpc\n enabled: true\n"+
|
||||
" - slug: spc\n rpc: http://x/rpc\n enabled: false\n"+
|
||||
" - slug: osage\n rpc: http://x/rpc\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg, err := LoadConfig(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := map[string]bool{"cchain": true, "spc": false, "osage": true}
|
||||
for _, c := range cfg.Chains {
|
||||
if c.On() != want[c.Slug] {
|
||||
t.Errorf("%s: On()=%v, want %v", c.Slug, c.On(), want[c.Slug])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user