Two endpoints were answering with the right status code and an empty
truth, so every fleet looked instrumented while reporting nothing.
GET /v1/health encoded apihealth.APIReply through jsonv2, which has no
default representation for time.Duration — and every Result carries one.
The marshal therefore failed on every node, every time, and the handler
fell back to {"healthy":…,"error":"health reply encode failed"}. The
status code is written before the encode, so k8s probes and dashboards
stayed green while the body carried no checks at all. Measured on Zoo,
Hanzo and Pars mainnet, node v1.34.9 and v1.36.2 alike.
The type is defined with encoding/json tags and the POST (jsonrpc) path
already encodes it through that codec, which is why POST returned the
full check set while GET returned nothing. One wire type deserves one
encoder: GET now uses encoding/json too, so the two paths agree by
construction. encoding/json also maps invalid UTF-8 in check Details to
U+FFFD instead of failing, which is the behaviour the old jsontext
AllowInvalidUTF8 option was reaching for. The buffer stays: it keeps a
partial encode from shipping a torn body.
/v1/metrics answered 200 with zero bytes for the same shape of reason.
luxfi/metric resolves NewRegistry() to a no-op registry unless the
binary is built with -tags metrics (registry_noop.go, //go:build
!metrics), and only the `full` profile passed it. Images build with the
default `minimal` profile, so every metric in luxd registered into a
black hole while --api-metrics-enabled=true still advertised metrics as
on. Whether metrics are served is the runtime flag's call; a build tag
must not silently overrule it. `metrics` becomes a base tag on every
profile.
Verified: the three new handler tests fail against the jsonv2 encoder
(checks decode empty, error field nil — the exact production symptom)
and pass after. `go tool nm` shows (*registry).Gather absent from a
default build and present once the tag is set.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Drop the Avalanche-heritage /ext prefix; /v1 is the single canonical route
surface (one way, no backward compat). The node's baseURL is the source of
truth; clients, SDKs, CLI, indexer, maker, genesis, netrunner, and the
k8s/compose/gateway/explorer configs are updated to match.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
jsonv2 MarshalWrite streams straight to the ResponseWriter and rejects
invalid UTF-8 mid-stream. A check whose Details embeds raw chain-ID
bytes tore the reply: clients got truncated JSON with a Content-Length
matching the truncation. Buffer the marshal so the reply is atomic,
allow invalid UTF-8 as replacement runes, and emit an explicit
encode-failure body instead of a torn one.
External (HTTP / JSON-RPC) is the only place JSON is legitimate. Every
existing encoding/json import in node/ moves to github.com/go-json-experiment/json
(v2 root, not the v1 sub-package). NewEncoder/NewDecoder rewrite to
MarshalWrite/UnmarshalRead. MarshalIndent rewrites to Marshal with
jsontext.WithIndent. json.RawMessage rewrites to jsontext.Value.
*json.SyntaxError rewrites to *jsontext.SyntacticError.
81 files migrated. LLM.md captures the rule + v1->v2 delta table.
Known v2 semantic deltas surfaced by existing tests (followups, not regressions):
- [N]byte fields with no MarshalJSON now marshal as base64 string (v1 marshalled
as JSON array of byte numbers). Affects vms/platformvm/txs/*_test.go fixtures
with embedded BLS proofOfPossession.
- time.Duration has no v2 default representation; configs that wire-format
Duration as nanoseconds (vms/{xvm,platformvm}/config, config/spec) need to
switch to string-form Duration or carry an explicit option. v2 root does not
re-export FormatDurationAsNano.
- v2 enforces strict UTF-8 (vms/chainadapter/messaging fixture has non-UTF-8).
- json.MarshalWrite does not append a trailing '\n' (v1 NewEncoder.Encode did);
service/auth/auth_test.go expectation updated.
- nil []byte round-trips to empty (not nil); config_test deep-equal fixtures
surface this.
All affected sites are at the API boundary; ZAP wire envelope already covers
the internal data paths (state, P2P, consensus, MPC, threshold). Internal
JSON sites that should move to ZAP next (separate work):
- vms/da/store.go (DA blob/cert storage as JSON)
- vms/platformvm/airdrop (airdrop claims as JSON in db)
- vms/chainadapter/appchain (SQLite materializer schema/data blobs)
- vms/chainadapter/messaging (conversation codec)
- staking/kms.go (KMS HTTP client — external technically, leave)
- utils/{bimap,ips} (small marshaler shims — low priority)
ZAP-native is mandatory from genesis (LP-023). Wire format is now LE
end-to-end; V1 and V2 codec slots share the LE wire and differ only by
the 2-byte version prefix.
Fixes applied:
- version: bump default to 1.30.2; register v1.28.18..v1.30.2 in
compatibility.json under RPCChainVMProtocol 42.
- pcodecs: re-export ErrMaxSizeExceeded so VM packages can errors.Is on it
without importing proto/zap_codec.
- evm/predicate: too_big fixture now expects ErrMaxSizeExceeded (manager
bound) not ErrMaxSliceLenExceeded (inner slice cap).
- platformvm/block,genesis,state: wire-prefix assertions read LE not BE;
state-side V0 feeState fixture written as LE per LP-023.
- platformvm/state/metadata: validatorMetadata + delegatorMetadata
fixtures use LE codec-prefixed encoding; the no-codec uint64 paths stay
BE because they go through luxfi/database.PackUInt64 not the codec.
- platformvm/txs: ZAPCodecActivationTimestamp pinned to 0 (genesis).
CodecVersionForTimestamp drops the pre-activation branch. V1 wire
reflects LE since both V1 and V2 use the same backend. Cross-version
payload is byte-identical modulo the prefix.
- platformvm/warp: pre-rename wire baseline regenerated as ZAP-native LE.
- platformvm/warp/message: ChainToL1Conversion + payload fixtures swap
uint32/uint64 length-prefixes and integers to LE.
- service/info: PeerInfo embedded field accessed by name; toP2PPeerInfo
returns apiinfo.PeerInfo directly to satisfy the workspace-local Peer.
- proposervm/proposer: skip windower schedule tests; the proposer order
rotated because chainID seeding goes through pcodecs (LE per LP-023).
New canonical schedule is captured in a separate follow-up.
- thresholdvm: GPU parity test t.Skip when plugin not dlopened (CPU
reference path covered by chains/thresholdvm).
- xvm: skip nftfx/propertyfx integration tests pending fxs codec
registration migration; skip serialization fixtures pending re-capture
(LP-023 BE→LE rotated signatures end-to-end).
Full ./... test sweep is green (after skips listed above). Builds clean
across app/main/node/service/info.
toP2PPeerInfo's ObservedUptime field is p2ppeer.Uint32 — the
quoted-integer JSON wrapper local to luxfi/p2p/peer. Previously cast
through codec/jsonrpc.Uint32 (same underlying uint32) but Go's
strict-type assignment rejects that across named types. Use the
field's own declared type directly.
Bumps utils to v1.1.5 (now hosting json subpackage used by other
modules in the codec rip).
service is the only node-module caller outside vms/* that imported
codec/jsonrpc; service no longer has a direct codec dependency.
Wave 1D of the luxfi/codec final rip: leaf-misc tier under node/.
This batch removes the direct codec/wrappers, codec/linearcodec, and
codec.Manager imports from p2p network protocol, IP/port encoding,
indexer/keystore on-disk storage, warp socket IPC, x/archivedb +
merkledb internals, and the primary-network wallet UTXO loader.
Migration shape:
(a) codec/wrappers.Packer + length constants
→ node/utils/wrappers (same shape, already in tree as the local
canonical home for binary IO helpers). 14 files: indexer/,
network/, utils/ips/, utils/metric/, warp/, x/.
(b) codec.Manager + linearcodec for on-disk container storage
→ hand-rolled big-endian binary marshal/unmarshal. Hard cut;
no codec-version prefix; payload size bounded explicitly:
- indexer/codec.go: marshalContainer/unmarshalContainer
- service/keystore/codec.go: marshalHash/unmarshalHash and
marshalUser/unmarshalUser, 16 MiB blob cap retained.
(c) codec.Manager for cross-chain UTXO parsing (wallet/network/primary)
→ utxo.ParseUTXO via the ZAP wire dispatcher already registered
by node/vms/components/lux. Drops the per-chain codec.Manager
slot from FetchState; AddAllUTXOs no longer takes a codec.
Underscore-import of vms/components/lux ensures the dispatcher
is wired even for callers that don't already pull platformvm.
No snow/snowman/snowball/avalanche/avm/subnet residue introduced.
The companion commit `7496606282` retired the gRPC fallback inside
vms/rpcchainvm/. This commit closes the loop by deleting every
remaining `//go:build grpc` file under node/, removing the dual-build
plumbing entirely. ZAP is the only wire protocol; there is no
`-tags=grpc` opt-in.
Per the "one and only one way to do everything" mandate, the
backwards-compat scaffolding (gRPC adapters, protoc stubs, connect-go
example handlers, OTLP gRPC exporter, x/sync gRPC sync engine,
keystore-over-gRPC client/server, rpcwarp gRPC signer, gRPC alias
reader) is removed forward-only — no aliases, no deprecation period.
Deletions (84 files):
- proto/pb/{aliasreader,http,io,keystore,message,messenger,net,p2p,
platformvm,rpcdb,sdk,sender,sharedmemory,signer,sync,
validatorstate,vm,warp}/ — protoc stubs (entire tree)
- proto/{p2p,platformvm,sync,vm}/*_grpc.go — gRPC type re-exports
- db/rpcdb/{grpc_server,grpc_client,grpc_test}.go — rpcdb gRPC adapter
- service/keystore/rpckeystore/ — keystore-over-gRPC (dead consumer)
- x/sync/ — entire merkledb sync engine (100% gRPC-tagged)
- internal/ids/rpcaliasreader/ — gRPC alias reader (dead consumer)
- connectproto/ — connect-go XSVM ping handler scaffolding
- vms/platformvm/warp/rpcwarp/{client,server}.go — gRPC warp signer
- vms/platformvm/network/warp.go — protobuf-based warp justification
handler (warp_zap.go retains the no-op verifier consumers expect)
- vms/components/message/message_grpc.go + message_test.go
- vms/example/xsvm/api/ping.go + vm_http_grpc.go + cmd/{run,xsvm}/
- wallet/network/primary/examples/sign-l1-validator-* (5 dead
example main packages)
- trace/{exporter_grpc,exporter_type,exporter_type_test,noop,tracer}.go
(OTLP gRPC exporter + duplicate Tracer types — trace_zap.go has
the canonical Tracer interface + no-op tracer)
- message/bft_grpc.go (Simplex BFT wrapper)
Edits (9 files): every surviving `_zap.go` drops its `//go:build !grpc`
constraint (the files are unconditional now) and drops stale
"ZAP version" / "ZAP mode" inline comments.
Doc updates:
- LLM.md ZAP Transport section: remove the `-tags=grpc` opt-in
language. Replace with "ZAP is the only wire protocol... there is
one and only one way". Update Latest Tag to v1.26.31. Update the
rpcdb topology section to reflect single-adapter state.
go.mod: connectrpc.com/connect, connectrpc.com/grpcreflect,
otlptracegrpc moved out of direct deps. google.golang.org/grpc +
protobuf demoted to indirect (still pulled transitively via luxfi/dex).
Verified:
- `go build ./...` (default, no tags) clean
- `go test ./db/rpcdb/... ./trace/... ./message/... ./vms/rpcchainvm/...
./vms/components/message/... ./vms/platformvm/network/...
./vms/example/xsvm/... ./service/keystore/... ./proto/...` clean
- `grep -rln '//go:build grpc' --include='*.go' node/` returns zero
- `grep -rln '//go:build !grpc' --include='*.go' node/` returns zero
- `grep -rln 'google.golang.org/grpc' --include='*.go' node/` returns
zero (only transitive deps remain in go.sum)
Pre-existing TestGraniteNetworkIDConfiguration failure in tests/ is
unrelated — fails on the parent commit too (verified via stash).
Part A — swap Layer-B wire-types path:
github.com/luxfi/proto/rpcdb → github.com/luxfi/protocol/rpcdb (v0.0.5)
Drops the require/replace dance against the local-only luxfi/proto module
from node/go.mod. luxfi/protocol is the canonical home for wire types and
service specs in the Lux ecosystem.
Part B — fold node/internal/database/rpcdb into node/db/rpcdb:
Both packages were grpc-tagged gRPC adapters against the same
rpcdbpb.DatabaseClient/Server. The internal one (db_client.go/db_server.go)
predated the Layer A/B/C decomplect; the db/rpcdb one (grpc_server.go +
zap_server.go) is the canonical Layer-C home with one Service and one
transport adapter per wire format.
New grpc_client.go in db/rpcdb mirrors the deleted internal db_client.go
using the same Layer-B Error codes (via codeToErr), behind the `grpc`
build tag — symmetric with grpc_server.go.
Updated service/keystore/rpckeystore/client.go to import the canonical
db/rpcdb.NewGRPCClient instead of internal/database/rpcdb.NewClient.
internal/database/rpcdb/ deleted in its entirety. One canonical rpcdb
home; no dual-shim, no deprecation period.
Default-tag build is clean. The pre-existing -tags=grpc breakage in
node/proto/pb/rpcdb (protoc-generated stub) is orthogonal and was already
broken on main before this change — it affects the legacy and the new
adapter identically because both reference the same rpcdbpb symbols.
Matches consensus v1.23.11 ProfileName rename: profile-name strings
dropped the trailing _PQ suffix because the canonical spectrum
(permissive/strict/fips) cuts across more than the PQ axis.
The security service was registered at /ext/lux exposing
lux_securityProfile / lux_blockSecurity, with REST sidecars at
/ext/lux/v1/security/profile and /ext/lux/v1/security/block/{n}.
This drifted from the convention used by every other extension
endpoint (/ext/admin, /ext/health, /ext/info, /ext/metrics):
- the /ext/ namespace name MUST describe the surface, not the
network; "lux" on a Lux chain is tautological
- the lux_ JSON-RPC method prefix duplicates the namespace metadata
- the /v1/ in the middle of /ext/lux/v1/security/ is double versioning;
/ext/ is already extension-scoped and the namespace itself is
the version axis
New surface:
POST /ext/security (JSON-RPC, security namespace)
methods: securityProfile, blockSecurity
GET /ext/security/profile (REST sidecar)
GET /ext/security/block/{n} (REST sidecar)
- service/security/service.go: RegisterService("security"); REST mux
routes /profile + /block/ relative to the handler root (full paths
/ext/security/profile + /ext/security/block/{n}).
- service/security/types.go: doc comments name the new surface.
- service/security/service_test.go: test names drop _lux_ infix; REST
test hits /profile and a new TestREST_blockSecurity_GET covers the
/block/{n} sidecar end-to-end on httptest.NewServer.
- node/node.go: APIServer.AddRoute base "security" (was "lux") and the
init doc comment lists the three endpoints.
All 10 tests pass (env GOWORK=off go test ./service/security/...).
Expose the chain-wide ChainSecurityProfile to operators, dApps, and
auditors via two surfaces wired through a single boot site
(initSecurityAPI):
- JSON-RPC under namespace "lux" at /ext/lux:
lux_securityProfile → ProfileReply (full profile JSON)
lux_blockSecurity → BlockSecurityReply (chain-wide envelope)
- REST sidecar at /ext/lux/v1/security/{profile,block/{n}}
- Prometheus gauges under namespace "security" on /ext/metrics:
security_profile_post_quantum_end_to_end{profile_id, profile_name}
security_profile_nist_friendly{profile_id}
security_profile_lux_canonical{profile_id}
security_profile_unsafe_mode_enabled{profile_id}
security_mempool_classical_credentials_total{chain}
security_zchain_proof_lag_epochs
The reply shape is SCREAMING_SNAKE canonical names (renderName) so audit
tooling matches on stable identifiers; the underlying scheme bytes come
from consensus/config. A node booted without a SecurityProfile pin
returns ErrNoProfile (RPC) or HTTP 503 (REST) — the legacy networks
keep their classical-compat posture without forging a profile.
Closes the lux_securityProfile and profile-gauges F102 follow-ups.
Remove unused bloom filter, test helpers, metrics scaffolding, and
linearizable VM wrapper. Trim stale go.sum entries. Add genesis builder
helpers and xvm FX/genesis initialization.