Compare commits

..
48 Commits
Author SHA1 Message Date
zeekayandHanzo Dev bd2edc135f fix(chains): chunked ancestry descent — a halted fleet must still name its frontier
bootstrapNamingWindow was one constant doing THREE jobs: the per-fetch
transport size, the total ancestry budget, and a health heuristic. That
conflation wedged mainnet 96369 with no way out.

nameFrontier is already ava-equivalent — it does not count identical tip
IDs, it builds a union index from each reported tip's ancestry and gives
"global credit" (every tip vouches for every block on its chain), which
is GetAccepted-at-height semantics derived LOCALLY from hash-linked
ancestry instead of trusting a peer's assertion. Strictly stronger: a
peer can lie in a reply, it cannot forge a parent chain.

The defect was that the vouching walk stopped after ONE 256-block fetch.
Measured live, per-node in-pod:

  luxd-1 -> 1098726 (1 responder)   luxd-3, luxd-4 -> 1098191 (2)
  1098191 IS an ancestor of 1098726; luxd-3/luxd-4 `latest` IS 1098191,
  so no competing branch exists anywhere.

The ⅔-of-RESPONDER floor is 2, and 1098191 should have earned 3 — its
two direct backers plus luxd-1 vouching via ancestry. But the gap is
535 blocks, the single 256-block fetch never reached down that far, so
it earned only 2, and `> floor` rejected it. Nothing named, every retry,
forever. The node sat at height 0 and the fleet could not regain quorum.

The health heuristic ("a ⅔-common height this far below the highest tip
is not a healthy bleeding-edge split") is true for a LIVE chain and
false for a HALTED one, where the skew between a straggler and a node
that ran on alone is arbitrarily large and perfectly healthy. Recovery
from a halt is exactly when this path matters most, and it was disabled
precisely then.

  - NamingWindow stays 256, now documented as the per-FETCH transport
    bound only (Ancestry returns full blocks; one fetch must fit a
    network message).
  - MaxNamingDepth (32768) is the new TOTAL walk budget, in window-sized
    chunks. Purely a resource bound: safety comes from hash-verified
    ancestry, MinResponders distinct voters, the ⅔-of-responder floor,
    and full re-Verify on descent — none of which depend on it.
  - The walk now checks ctx each chunk, so the deadline binds the WALK
    and not merely each fetch (a peer serving a long fabricated chain
    cheaply would otherwise run the whole budget before anyone looked
    at the clock).

Tests — the matrix, all executed:
  H_HaltSkewDeeperThanOneWindow      535-block gap (asserts gap > one
                                     window, else it proves nothing)
  H_HaltSkewBeyondDepthStillFailsSafe  skew past the budget still names
                                     nothing rather than guessing
  1 high + 2 low + 2 unavailable  -> names the common ancestor
  3 high + 2 unavailable          -> names the high tip, not an ancestor
  fabricated tall tip + 2 low     -> SAFE HALT. A 9,000,000-height chain
                                     that does not link earns credit only
                                     on its own chain and can never win
                                     on height; the honest pair reach
                                     exactly the floor and strict `>`
                                     refuses. 1 of 3 responders can cost
                                     LIVENESS, never SAFETY.
  2 conflicting equal-height branches -> halts, never picks by height

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 11:03:06 -07:00
zeekayandHanzo Dev bdd7179164 docs(LLM.md): the dedup-capability branch DOES touch proposervm — scope the no-upstream-help claim to the build path
The upstream-review entry claimed nothing in the delta or its branches
touches proposervm. The delta half is proven (0 of 56 files); the branch
half was false: containerman17/proposervm-dedup-capability rewrites the
block store (inner-bytes dedup) and adds a boot-repair arm for the same
unclean-shutdown window OuterCommittedInnerNot pins. Storage-layer work,
not the build-path preferred fetch — the vm.go:380 verdict stands, but
whoever attacks it should know the one adjacent branch by name.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 09:26:05 -07:00
zeekayandHanzo Dev 50fe597bf7 v1.36.37: pin evm v1.104.23 — C-Chain plugin gets the luxfi/vm v1.3.3 map-race fix
The v1.104.22 plugin (vm v1.3.1) dies with a concurrent-map fatal in
components/chain/state.go getCachedBlock under gossip ParseBlock load, and the
dead plugin is never restarted: the node stays Running while the C-Chain is
dead (zap: connection closed, 'cannot vote correctly'). Observed live on
devnet luxd-1 during the v1.36.36 rolling-upgrade drill.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 09:25:13 -07:00
zeekayandHanzo Dev 8873e231e8 test(proposervm): crash-copy cold-boot recovery armor
Copy the committed bytes out from under a RUNNING proposervm (no
Shutdown), boot a second, cold VM over the copy, and require: a clean
boot in Initialize order, source-equality of the finality pointer, fork
height and every height's envelope opened cold, and a successful build
whose outer parent is the recovered tip and whose inner parent is that
tip's inner block. The matrix covers nothing-accepted, first-accept,
a longer run, and a copy taken while the source keeps accepting; a
dedicated test pins the one crash window the accept path leaves open
(outer batch committed, inner accept lost) booting through the
roll-back arm and re-proposing.

Harness seams testVMOnBase + acceptRangeThroughProposervm added to
height_lag_repro_test.go; everything else reuses the real VM, State and
accept path.

Verified: full vms/proposervm package green; negative control (dropping
the accept-path db.Commit) fails all four persistence-bearing scenarios.

Prompted by reviewing the upstream reference delta
bcc851822d..c5d3c8aafe: zero code ports apply (nothing in it touches
proposervm), but its crash-recovery test discipline was worth carrying
over. Verdict recorded in LLM.md.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 09:13:48 -07:00
zeekayandHanzo Dev 2a724fb090 v1.36.36: no-op patch for rolling-upgrade drill — sync stale version.txt fallback (1.32.11 -> 1.36.36)
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 09:12:04 -07:00
zeekayandHanzo Dev 23d93bf60b docs(LLM.md): testnet 96368 rolled to v1.36.35 — and the two defects it took to get there
The proposervm repair landed exactly as devnet predicted (1453->1491 rebuilt on
first boot, twice), and restoring a 4th live C-Chain broke the 1779 freeze without
touching --consensus-quorum-size. But two blockers devnet never hit had to be
fixed first, and one of them is a landmine for every future image:

luxfi/evm main pinned luxfi/vm v1.3.1 while node v1.36.35 pinned v1.3.3, so the
map-race fix shipped in luxd and NOT in the C-Chain plugin that actually verifies
blocks. It killed testnet luxd-0 five minutes into the roll, and the readiness
probe could not see it: pod Ready, restarts=0, isBootstrapped(C)=true, RPC dead.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 09:03:30 -07:00
zeekayandHanzo Dev 8a797f4805 docs(LLM.md): record the v1.36.35 devnet roll result and the third, pre-existing proposervm stall
Both fixed defects are gone fleet-wide (0 CONSENSUS-CERTIFIED fatals, 0 map races,
restarts=0 on all five for 20+ min) and a transaction reached identical receipts on
all five nodes. Five-way tip parity is NOT met: luxd-2 and luxd-4 freeze on
proposervm vm.go:380 'failed to fetch preferred block', which is proven pre-existing
(mainnet v1.36.2 luxd-1: 2223 occurrences; testnet v1.36.24 luxd-3: 35693) and is
survivable — the stalled node still votes, so the chain kept finalizing to 1913.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 03:45:59 -07:00
zeekayandHanzo Dev 9f4819cf77 v1.36.35: repair the main-branch build break (zap-proto/http v0.3.0)
011e9bf99d bumped github.com/zap-proto/http from a pseudo-version to v0.3.0 and
pushed to main without a build. v0.3.0 is a breaking API change:

  Server.Handler   net/http.Handler   ->  fasthttp.RequestHandler
  NewTransport(addr)                  ->  Dial(network, addr)

so server/http/zap_listener.go stopped compiling and MAIN HAS BEEN UNBUILDABLE
SINCE. The on-cluster image build for v1.36.34 failed on exactly that line, which
is how it surfaced; v1.36.34 produced no image and its tag is deleted.

Repair: bridge the SAME net/http handler chain the HTTP listener serves with
fasthttpadaptor.NewFastHTTPHandler, so the two transports stay behaviourally
identical and only the wire encoding differs — one handler, one place. The
round-trip test now drives a real ZAP request over the wire through that bridge
using the fasthttp request/response pair.

  ok github.com/luxfi/node/server/http
     TestZapRPCListenAddr / TestStartZapRPCListener_Disabled /
     TestStartZapRPCListener_RoundTrip

Carries the v1.36.34 payload (consensus v1.36.12 + vm v1.3.3) unchanged.
Binary self-reports luxd/1.36.35.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 03:17:36 -07:00
zeekayandHanzo Dev 84a08c2b9c v1.36.34: consensus v1.36.12 + vm v1.3.3 — the two P0s the v1.36.33 roll unmasked
consensus v1.36.12 — five devnet validators os.Exit(1)'d on a benign state:
certified = head - 1 with no fork anywhere. Two defects, one crash: the finalize
path steered the VM with a stale local blockID (backwards, into the EVM's correct
accepted-irreversibility refusal), and the classifier called "head is certified at
its own height" a double-finalization without ever checking that `certified` was at
that same height. Fixed at the producer (steer at the live build anchor) and the
classifier (a certified head is only orphaned for a block our ledger does not
certify at its height). The EVM guard and the fail-closed halt are both intact.

vm v1.3.3 — the EVM plugin process died on an unrecoverable Go map fatal in
chain.State.getCachedBlock, reached from the ZAP RPC ParseBlock handler with no
chain lock. State now owns the lock for verifiedBlocks + lastAcceptedBlock. luxd
survives that fatal and keeps reporting healthy while its chain is gone, so it is
invisible to /v1/health and pod-Ready alike.

Binary self-reports luxd/1.36.34.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 03:13:14 -07:00
zeekayandClaude Opus 5 011e9bf99d deps: drop go.sum lines that disagree with the checksum log
These hashes were recorded from direct VCS fetches made while GOPRIVATE covered
github.com/luxfi/*, which switches off the module proxy and checksum
verification together. Tags moved afterwards, so what was written down no longer
matches what sum.golang.org holds, and the build refuses to verify.

proxy.golang.org still serves the originally-published bytes and those verify
against the log, so dropping only the disagreeing lines and re-tidying restores
the build with no version change. Lines that agree were left alone — this is not
a regenerated go.sum.

The mismatch followed the machine, not the repository: the same commit built
wherever the cache was clean. Any machine or CI runner that fetched luxfi
modules under the old GOPRIVATE holds the same poisoned entries.

Also moves zap-proto/http off the 2026-05 pseudo-version to v0.3.0. The listener
already wrapped its handler with fasthttpadaptor, which is what v0.3.0 Server
takes; the pinned version still declared net/http.Handler, so the two disagreed
and the package would not compile. The go.sum repair had to land first — go get
could not run while the stale luxfi/vm lines were still being verified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 02:14:22 -07:00
zeekayandHanzo Dev c39a85522d docs(LLM.md): correct the pChainHeight claim in the v1.36.33 entry
"Nothing in the verify path reads pChainHeight before the parent check" is wrong:
postForkCommonComponents.Verify reads child.PChainHeight() at block.go:144 and
returns errPChainHeightNotMonotonic BEFORE the inner-parent check at block.go:151.
The conclusion is unchanged and now stated exactly — that read is monotonicity only
(0 < 0 is false, so it passes), and every P-Chain-DEPENDENT check (epoch,
GetCurrentHeight, proposer window) is gated behind consensusState == Ready and sits
AFTER the parent check, so pChainHeight=0 cannot produce errInnerParentMismatch.

Also name the ids so the LpoYY reading is checkable: constants.PlatformChainID =
ids.PChainID ("111…P"), PrimaryNetworkID = ids.Empty ("111…LpoYY") —
luxfi/constants@v1.6.2 network_ids.go:64-65.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 01:06:32 -07:00
zeekayandHanzo Dev 1c92026c7e fix(admin): setLoggerLevel/getLoggerLevel actually move and report a logger's level
Both endpoints answered 200 OK having done nothing. SetLoggerLevel computed the
logger names and threw them away — `loggerNames := a.getLoggerNames(...); _ =
loggerNames` — and getLogLevels returned an empty map unconditionally, so
getLoggerLevel reported `{}` for every logger no matter what had been set. There
was no way to raise a running node's log level, which is why the 2026-07-28
devnet/testnet build-loop diagnosis had to be run off boot logs.

log.Factory already exposes exactly what is needed at the pinned v1.4.3:
SetLogLevel/SetDisplayLevel/GetLogLevel/GetDisplayLevel, all addressed BY NAME,
plus log.ToLevel to parse the argument. So:

- SetLoggerLevel parses both levels BEFORE taking the lock (a rejected level
  leaves every logger untouched) and applies only the levels the caller supplied,
  so omitting one keeps its value instead of resetting it to the zero Level.
- getLogLevels reads the factory it is reporting on.
- getLoggerNames is the ONE place either endpoint decides what it addresses, and
  it now refuses an empty name explicitly: loggers are addressed by name and
  log.Factory exposes no enumeration, so the "every logger" form cannot be
  served. Answering 200 OK for it is the bug, not the contract.

Known remaining gap, in the dependency not here: factory.SetLogLevel silently
ignores an unregistered name and GetLogLevel answers InfoLevel for it, so a
typo'd loggerName still succeeds quietly. Closing that needs luxfi/log to report
an unknown logger; it is not reachable from this repo.

logger_level_test.go drives the REAL log.Factory the node builds, not a double,
and asserts against the factory as well as the API reply, so a getLoggerLevel
that merely echoed the request could not pass. Both behavioural tests fail with
the pre-fix bodies restored; the arg-validation test passes on both sides.

No version bump and no new tag: v1.36.33 stays the exact artifact under review
for the devnet/testnet/mainnet roll. This rides the next release.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 01:04:02 -07:00
zeekayandHanzo Dev f7e501d021 docs(LLM): the v1.36.33 roll surface is the StatefulSet, and mainnet is not a clean control
Verified live 2026-07-28T07:5xZ while red-reviewing the v1.36.33 hand-off:

- lux-operator and lux-operator-devnet are 0/0, so luxnetworks.lux.cloud/luxd
  reconciles nothing and its tags are stale (mainnet v1.34.0 — in no registry;
  testnet v1.32.12). The live image is on the StatefulSet (mainnet v1.36.2 via
  kubectl-patch 07-25, devnet v1.36.25@sha256:ca497eff, testnet
  v1.36.24@sha256:91e2542b), all OnDelete. Rolling via the CR then deleting a pod
  reboots it on the OLD image — the C-Chain-dies-on-boot case.
- mainnet luxd-0/3/4 are frozen at 1098191 (block ts 2026-07-24T15:46:19Z) with
  3998/4000 log lines rebuilding height=1098196, luxd-2 has no C-Chain, and only
  luxd-1 mines (1098341, receipt status 0x1). Same hash at 1098191 on luxd-0 and
  luxd-1, so it is not a fork. The drop line is missing from the v1.36.2 BINARY
  (grep -c "built block failed verification" /luxd/build/luxd: 0 on mainnet,
  1 on devnet v1.36.25) — which is the only reason mainnet looked clean.
- The 4 broken mainnet pods are exactly those on ControllerRevision rev 147;
  luxd-1, the one that mines, is the only pod still on rev 144. Deleting it
  recreates it on the revision the others broke on.
- ghcr.io/luxfi/node:v1.36.2 corresponds to no git tag, so what mainnet runs is
  not reproducible from this repo.

Docs only: no source, no version change, no live change.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 01:00:46 -07:00
zeekayandHanzo Dev a57251c318 fix(proposervm): anchor the inner build parent, so a node stops rejecting the block it just built
devnet 96367 and testnet 96368 spent hours in a build→self-verify-fail→drop loop:
every proposer emitted "built block … height=1047" and immediately "built block
failed verification — dropping / inner parentID didn't match expected parent",
83–456 drops/min per node, with the accepted tip frozen two heights BELOW what the
builder kept proposing.

buildChild asked the inner VM for a block without first pointing the inner VM at the
parent's inner block. The inner VM builds on ITS OWN head (luxfi/evm: the miner reads
bc.CurrentBlock()); postForkCommonComponents.Verify requires
child.innerBlk.Parent() == parent.innerBlk.ID(). Build and verify read two different
pointers, one required to equal the other, and nothing asserted it at the moment it
was needed.

The head drifts without the proposervm's knowledge: verifying a GOSSIPED block whose
parent is the current head optimistically makes it the head (evm core/blockchain.go
writeBlockAndSetHead → newTip → writeCanonicalBlockWithLogs → writeHeadBlock), no
accept and no proposervm involvement. SetPreference cannot undo it either — it
short-circuits on an unchanged outer preference, so re-affirming the same tip never
re-pushes the inner preference. Nothing self-corrects; the loop is forever.

VM.anchorInnerBuildParent asserts the invariant where it is required: one inner
SetPreference immediately before delegating, from BOTH build sites
(postForkCommonComponents.buildChild and preForkBlock.buildChild — the transition
block is verified by the same check, and at the fork height every validator may emit
its own candidate, so the drift is the norm there). On a healthy node the inner
setPreference early-returns on current.Hash() == block.Hash(): one lookup, no state
change, no behaviour change. When it fails, the head is provably not the parent's
inner block, so refusing to build is strictly better than emitting a block this node
is guaranteed to drop.

This is NOT the P-Chain. info.isBootstrapped{"chain":"P"} is true on 15/15 nodes and
platform.getHeight is 0 on 15/15 including mainnet, whose built blocks also carry
pChainHeight=0; the health blob's "111…LpoYY" is the primary-network NET id, not the
P-Chain. Nothing in the verify path reads pChainHeight before the parent check.

build_inner_parent_test.go models the inner VM's three real head semantics
(build-on-head, verify-advances-head, SetPreference-reorgs-head) and fails without
this change on both build paths; TestBuildChild_HealthyHead_NoReorg pins the
no-behaviour-change half.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 00:43:07 -07:00
zeekayandHanzo Dev 2f6ef061ab chore(version): patch-bump 1.36.31 -> 1.36.32
Carries the proposervm finality-index fix. defaultPatch and the RPCChainVM
protocol-42 compatibility list move together, so a binary built without
ldflags reports the same version as the tag and TestCurrentRPCChainVMCompatible
stays green.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-27 19:45:05 -07:00
zeekayandHanzo Dev 7d2f01eb0c fix(proposervm): finality index can no longer fall behind the inner VM tip, and a node that is already behind boots and repairs itself
ROOT CAUSE. Every post-fork accept commits the outer envelope, its height
index entry and the last-accepted pointer in ONE versiondb batch BEFORE the
inner block is accepted, so the index can only run AHEAD. But the proposervm
has one accept path that moves the inner VM and writes nothing:
preForkBlock.Accept, whose acceptOuterBlk() is a no-op. Post-fork it was
reachable because BOTH proposervm block constructors fall back to it
silently — getBlock() when the id is not an OUTER envelope id (and the id the
consensus ledger records as canonical IS the inner block's id, see
postForkCommonComponents.CanonicalID), and ParseBlock() when the envelope
does not parse. Accepting one such block advanced the inner VM and left the
index stranded. Nothing complained, because nothing asserted the invariant at
the moment it was violated — it was only checked at the NEXT boot, where
repairAcceptedChainByHeight refused to start and killed the chain:

  VM initialization failed error="failed to repair accepted chain by height:
  proposervm finality index (height 6, id ns5qGN4i...) is BEHIND the inner VM
  tip (height 98, id TUTR74eA...)"
  non-critical chain failed to initialize ... chainAlias=C

The writes were never issued at all — this was never a persistence problem.

PREVENTION. One predicate, vm.refusePreForkAfterFork(height), used by both
seams so they cannot disagree: post-fork, a block at or above the recorded
fork height is never a pre-fork block, so getPreForkBlock refuses to build one
and preForkBlock.acceptOuterBlk refuses to accept one. Before the fork
(no fork height recorded) both are unchanged no-ops. acceptPostForkBlock also
warns on a non-contiguous index so a hole is named where it opens.

RECOVERY (height_backfill.go), outer-only — no inner re-execution, no EVM
rollback, no resync:
  1. rebuildOuterIndexFromStore re-derives the index at boot from envelopes
     already in this node's block store, binding each candidate to the inner
     block WE accepted at that height and to the envelope at height-1.
  2. If that cannot reach the tip the chain STARTS anyway, loud and
     build-gated (a node with a hole must not propose), and heals through
     BackfillOuterBlock — which is also offered every envelope arriving via
     ordinary ParseBlock traffic, so a damaged node repairs itself from peers
     with no new transport and no operator step.
The finality pointer is only ever moved FORWARD onto a proven envelope and is
never dropped: DeleteLastAccepted would make LastAccepted() fall back to an
inner-namespace id whose ParentID is contiguity-incompatible with the
network's outer wrappers, silently wedging the node at the inner tip forever.

TESTS. height_lag_repro_test.go reproduces the lag using only pre-existing
API; on the unfixed code it prints the divergence and then the verbatim
production init error, and passes after the fix. height_backfill_test.go
proves both recovery paths, the build gate, the self-heal from parse traffic,
and the two rejection cases (wrong inner block, out of order).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-27 19:43:37 -07:00
zeekayandHanzo Dev c76fcda71d fix: point keyutil at kms/pkg/mnemonic, the canonical loader
keys.LoadMnemonicFromKMS was deleted from luxfi/keys at v1.3.0 and this
call site never moved, so the example helper has not compiled since:

    keyutil.go:76:25: undefined: keys.LoadMnemonicFromKMS

keyutil backs eight wallet examples, so the break is not contained to
one program.

The loader was not lost — it moved to kms/pkg/mnemonic deliberately.
keys must not import kms, because kms already imports keys for
ServiceIdentity envelopes, and the back edge would close an import
cycle. mnemonic.LoadFromKMS keeps the same behaviour and adds a
ServiceIdentity parameter; nil is the documented value for a caller
trusting the network boundary rather than a signed application-layer
envelope, which is what this path already relied on.

luxfi/kms was already an indirect dependency (v1.12.4); this makes it
direct at v1.12.10. The MNEMONIC-env local is renamed to `phrase` so it
stops shadowing the package inside the same function.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-27 13:47:38 -07:00
zeekayandHanzo Dev e1e3917772 Dockerfile: pin EVM v1.104.22 — realign plugin api/vm/geth with node, land the fee-split seam
Two defects in one ARG.

1. api mismatch. Node main pins luxfi/api v1.1.1, vm v1.3.1, geth v1.20.1.
   EVM_VERSION was pinned at v1.104.9, whose go.mod is api v1.0.16 / vm v1.2.6
   / geth v1.17.12. That is the same InitializeResponse decode mismatch this
   file already documents for v1.104.8, just inverted: host and plugin must sit
   on one api line or every mgj786 chain fails to initialize. v1.104.22 is
   api v1.1.1 / vm v1.3.1 / geth v1.20.1 — an exact match.

2. fee split absent. The C-Chain fee-split seam (core/fee_split.go creditTxFee,
   extras.FeeSplitTimestamp, extras.FeeRewardVault 0x0100..0002) first ships in
   evm v1.104.14. Every node image up to and including v1.36.25 bakes an evm
   below that line, so encoding/json silently discards the genesis
   "feeSplitTimestamp" key. Measured on devnet 96367 (node v1.36.25, genesis
   feeSplitTimestamp 1785133547, long past): a mined transfer credited 100% of
   its fee to the block coinbase and left the reward vault at exactly 0 — no
   reward accrual, no burn. Confirmed against the running plugin binary:
   feeSplitTimestamp / creditTxFee / FeeRewardVault all grep 0 times, while
   feeConfig and cancunTime hit.

The split remains dormant wherever feeSplitTimestamp is absent (mainnet), where
creditTxFee takes the unchanged legacy coinbase path, so this bump is
behaviour-preserving there.

Note for operators upgrading a LIVE chain that already carries a past-dated
feeSplitTimestamp: extras.checkConfigCompatible rejects nil -> set once the
timestamp is behind head, so forward-date feeSplitTimestamp (or re-genesis)
in the same change as the image bump.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-27 11:47:15 -07:00
zeekay e7137ed28d Merge remote-tracking branch 'origin/main'
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-26 20:06:15 -07:00
hanzo-devandHanzo Dev 3398024a19 build: reconcile tools go.sum with the module graph
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-26 20:05:15 -07:00
zeekayandHanzo Dev e77b696662 fix(deps): re-pin luxfi/consensus to v1.36.10 — v1.36.9 silently wedges a chain
main pinned consensus v1.36.9, which contains the silent-rebuild-storm bug that
took devnet down for four and a half days. Any network that upgraded from HEAD
would have wedged the same way.

The bug: buildBlocksLocked keeps pendingBuildBlocks after a failed build, and
proposervm truncates the block timestamp to a whole second — so every rebuild
inside that second re-mints an IDENTICAL blkID. consensus.AddBlock rejects it as
"already exists" and a bare `continue` skips Propose/RequestVotes entirely. The
block is never sent, never voted, never decided, while pending txs keep
re-notifying. Measured on devnet: one node re-built the same blkID 2,287 times
at height 512, with ZERO "proposed block to validators" and ZERO "finalized
block via quorum cert" lines across 200k log lines fleet-wide.

It also leaves a durable trap. The on-disk vote-once guard binds those storm
blkIDs to heights that can never be re-proposed, so reserveSlotForSign refuses
every later block at those heights — the chain stays wedged even after the
binary is fixed. Devnet needed a hand-repaired guard on all five nodes to
recover; that repair has no code path yet (the voteguard docstring points at
engine/chain/lock_migration.go, which does not exist).

This is a REGRESSION, not a stale pin: v1.36.24 and v1.36.25 both pinned
v1.36.10 and finalize normally — devnet runs v1.36.25 today and is producing
blocks. v1.36.26, v1.36.27, v1.36.28 and HEAD went back to v1.36.9.

v1.36.10 is the latest consensus release, so this moves forward. Verified
against an EMPTY module cache (the cold path the in-cluster builder takes), and
./vms/... and ./chains/... build clean.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-26 19:51:24 -07:00
zeekayandHanzo Dev eb82872612 version: backfill v1.36.11..31 into the RPCChainVM-42 compatibility set (v1.36.31)
TestCurrentRPCChainVMCompatible has been red since v1.36.11: every release
since then bumped defaultPatch without registering the version against
RPCChainVMProtocol 42, so version.Current was absent from its own
compatibility set. Protocol 42 has not moved across that range — the list was
simply never updated. Backfilled through v1.36.31 (v1.36.29 omitted: that tag
was pushed off a pre-rebase commit and deleted, it is not a release).

v1.36.30 carries the health fix but was tagged before this backfill, so its
tree still fails ./version. v1.36.31 is the green tag and the rollout target.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-26 17:39:52 -07:00
zeekayandHanzo Dev 4fce5875c5 v1.36.30: cut the health-truth fix on main
v1.36.29 was tagged off a pre-rebase commit that never reached main and has
been deleted. Forward only: the release is v1.36.30, on main.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-26 17:36:39 -07:00
zeekayandHanzo Dev 304717c199 health: name the CHAIN that is unbootstrapped, not the net (v1.36.29)
/v1/health's `bootstrapped` check publishes chains.Nets.Bootstrapping()
verbatim as its message. Nets.chains is keyed by NET id, and the aggregate
appended the map key rather than asking the net which of its chains had not
converged. So every stuck primary-network chain surfaced as one ID,
11111111111111111111111111111111LpoYY — constants.PrimaryNetworkID, i.e.
ids.Empty — a "chain" the chain manager has never heard of. An operator who
went looking for it got "there is no chain with alias/ID", and N stuck chains
collapsed into a single indistinguishable entry that named none of them.

Measured on lux-devnet luxd-0 (v1.36.23), POST health.health:
  "bootstrapped":{"message":["11111111111111111111111111111111LpoYY"],
   "error":"chains not bootstrapped","contiguousFailures":3213}

The net owns the bootstrapping set, so the net is what can name those chains:
nets.Net grows Bootstrapping() []ids.ID and chains.Nets aggregates those
instead of its own keys. One place holds the set; one place reads it.

The existing TestNetsBootstrapping asserted the defect (require.Contains
bootstrapping, netID) — that assertion is why it survived review. It now
demands the chain ID and refuses the net ID.

This also cuts the first tag containing dada5a31, the GET /v1/health encoder
fix: apihealth.APIReply carries a time.Duration per check, jsonv2 has no
default representation for it, so every GET degraded to
{"healthy":…,"error":"health reply encode failed"} while the status code still
looked right. Reproduced here directly —
  json: cannot marshal from Go time.Duration within "/checks/network/duration"
That fix landed on main on 2026-07-25 but no tag ever carried it: v1.36.28
points at 52de3948, seven commits behind. The fleet runs v1.36.2/23/24, all of
which predate it.

Tests (GOWORK=off CGO_ENABLED=0):
  chains  ok  — TestNetsBootstrappingReportsChainsNotNets fails against the
                old aggregate with exactly ids.Empty in the list
  nets    ok  — TestNetBootstrappingNamesTheChains
  health  ok  — the three handler tests fail against the jsonv2 encoder
                (checks decode empty) and pass against encoding/json

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-26 17:36:07 -07:00
Hanzo Dev ad76f2e709 Makefile: select GOEXPERIMENT=none on WSL2 so make build yields a working luxd
The Go 1.26 runtimesecret experiment SIGSEGVs at startup under the WSL2 kernel
(go1.26.3, go1.26.4). Detect WSL from /proc/sys/kernel/osrelease and drop the
experiment only there; all other platforms keep stack/register zeroing.
2026-07-26 14:49:50 -07:00
Hanzo Dev 26d419c3ad build: disable the runtimesecret GOEXPERIMENT under WSL2
The Go 1.26 runtimesecret experiment (stack/register zeroing for forward
secrecy) SIGSEGVs at startup on the WSL2 kernel, confirmed on go1.26.3 and
go1.26.4. Detect WSL via /proc/sys/kernel/osrelease and select GOEXPERIMENT=none
there; every other platform keeps forward secrecy. Plain `make build` now
produces a working luxd on WSL.
2026-07-26 14:44:30 -07:00
zeekay 6c39b7d391 chore: sync working tree
Commits 1 outstanding change(s) that were sitting uncommitted.
No build artifacts and no secrets in the changeset (both checked).
2026-07-26 10:02:56 -07:00
zeekayandHanzo Dev dada5a3169 health,build: make /v1/health and /v1/metrics report reality
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>
2026-07-25 15:10:46 -07:00
zeekayandHanzo Dev 3a44f0dcd3 genesis: refuse an M-Chain policy the validator set cannot satisfy
M-Chain's committee is the validator set, and RunKeygen refuses a policy
needing more parties than the committee has. mainnet's mchain.json asked
for 7-of-10 against five genesis validators, so a custody key could never
have been generated — the failure would have surfaced the first time
someone tried to bridge, not at deploy.

Adds the invariant as a test over every network's built genesis, and
picks up luxfi/genesis v1.16.4 where the policies are sized to fit
(3-of-5 on mainnet/testnet/devnet, 2-of-3 on localnet).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-25 14:59:04 -07:00
zeekayandHanzo Dev d5eff75934 genesis: install the M-Chain plugin under the vmID genesis actually declares
The build installed M-Chain's plugin binary as
tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t — the CB58 of the
retired `thresholdvm` identifier — while the genesis builder declares the
chain with constants.MPCVMID (qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS).
The plugin registry resolves a CreateChainTx's vmID to an implementation
by filename, so the two never met: M-Chain was declared in genesis and no
node could start it. The Dockerfile comment had already been renamed to
"mpcvm" without the CB58 being recomputed, which is why it read as correct.

Fixes all five sites (plugin build target, the build-verify list, the
runtime COPY, the comment, and publish_plugin_set.sh) and adds
genesis/builder/mchain_test.go, which pins the vmID literally and asserts
M-Chain is present in the height-0 chain set for mainnet, testnet and
local. A vmID is an immutable one-way door once a chain is created with
it, so it is now covered by a test rather than by five copies of a string.

Bumps luxfi/genesis to v1.16.3, where mchain.json states its quorum as
"policy": "7-of-10" instead of a bare mpcThreshold that reads as the
signer count to an operator and as the polynomial degree to a library.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-25 14:53:07 -07:00
zeekayandHanzo Dev 66d5940f88 Dockerfile: let the C-Chain plugin link cevm, and fix the cevm fetch
Two things kept the cevm backend from ever reaching a running node.

The fetch pointed at luxcpp/cevm v0.19.0, which does not exist — the libs
publish from the private lux-private/cevm repo, and unauthenticated downloads
404 there. Point it at v0.51.10 in that repo and authenticate with the same
`ghtok` secret the private go modules and lux-accel already use. It stays
best-effort, so a build without the token behaves exactly as before.

Separately, the C-Chain plugin builds with CGO_ENABLED=0 and no tags, so it is
always the pure-Go EVM regardless of what luxd itself links — which is why
production images carry no cevm at all. That build now honours EVM_CGO and
EVM_TAGS, defaulting to today exact behaviour; EVM_CGO=1 EVM_TAGS=cevm is what
makes AutoEVM resolve to CppEVM. The libraries are already in this stage (the
plugin section shares the builder stage rather than starting a new FROM).

Defaults unchanged, so the in-flight v1.36.12 fleet build is byte-identical.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-25 13:43:32 -07:00
zeekay e22009db91 chore(node): one way only — delete four dead duplicate paths
Each removed package was a SECOND way to do something that already has a
canonical first way. Zero importers workspace-wide; verified identical build
output before/after (pre-existing keyutil breakage unchanged).

- chains/rpc      1250 lines whose own header says 'This replaces lines
                  941-990' of chains/manager.go. The replacement never
                  happened; manager.go still registers handlers inline.
- node/config.go  265-line shadow of the canonical node/config/node/config.go.
- vms/mpcvm       self-described 'thin backward-compatibility alias' wrappers
- vms/dexvm       over github.com/luxfi/chains/*. No backwards compatibility,
                  only forwards perfection.

Simple made easy: one name, one home, one path.
2026-07-25 12:26:36 -07:00
zeekay 011c3df6bd chore(node): remove AI-slop write-ups and stale residue
- LAUNCH_CHECKLIST.md: pre-launch plan for v1.24.11, 154/154 boxes
  unchecked, zero references; superseded by LLM.md/CHANGELOG.md/RELEASE.md
  and NETWORKS.yaml + genesis/configs for the chain-ID/port tables.
- rename_app.sh, replace_imports.sh: spent one-off sed migrations. Both
  are now actively harmful — replace_imports.sh would rewrite the live
  github.com/luxfi/vm/manager import in vms/manager.go.
- gen_zoo_addr: stray 3.4MB darwin/arm64 build artifact committed at root;
  source preserved at cmd/gen_zoo_addr/gen_zoo_addr.go (.gitignore already
  covers the intended output path).
- .ci-status-check.md, .ci-trigger: dated CI-poke stamps, no workflow reads
  them.

Also removed (untracked): .claude/worktrees/ agent scratch — a 73MB
whole-tree duplicate that polluted every grep. Detached via git worktree
remove; node HEAD cf8e4c6f51 was an ancestor of main, and the nested
lux/evm worktree HEAD 7156c44f6 is release tag v1.104.12, so no work lost.
Deleted the two fully-merged worktree-agent-* branches it left behind.

No Go source, config, manifest, or .github/ file was touched.
2026-07-25 11:48:34 -07:00
zeekayandHanzo Dev 52de39485d fix(platformvm): gate P-chain uptime state.Commit on an actual write (v1.36.28)
Disconnect/updateUptimeLocked now return (mutated bool, err); VM.Disconnected commits only when the flush wrote uptime state. Before StartTracking (bootstrap churn) and for non-validator peers the flush is a no-op, so the prior unconditional Commit was an empty full-write+fsync on every such disconnect. Skipping it is correct: every writer under stateLock commits its own diff, so no orphaned write depends on the disconnect path.

RED round-2 verdict: SHIP-READY (H1 phantom verified, mutated-gate correct, H2 pre-existing/not-worsened, block-height orthogonal). Isolated from the uncommitted staking-KMS WIP, which is quarantined on feat/staking-kms-native pending its own Blue->Red.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-24 04:20:00 -07:00
zeekayandHanzo Dev e70f687c10 fix(node): serialize P-chain Connected/Disconnected + plumb real peer version (RED CRITICAL #1/#2)
The uptime event-delivery plumbing (node/chain_router.go + chains/manager.go
blockHandler) dropped two invariants avalanchego's handler upholds — the
consensus lock and the peer version — each a mainnet-fleet crash. The uptime
tracker itself (vms/platformvm/uptime_tracker.go) is RED-cleared and UNCHANGED.

CRITICAL #2 — P-chain state race (concurrent map writes -> fatal):
chainRouter dispatched Connected/Disconnected on the peer-lifecycle goroutine,
where VM.Disconnected -> tracker.Disconnect (state.SetUptime) + state.Commit
(state.write) ran concurrently with the block acceptor's state.CommitBatch
(state.write) on the engine accept goroutine — no shared lock, so ordinary peer
churn triggered Go "concurrent map writes" and crashed the P-chain node.
Fix: one VM-owned stateLock serializes every commit of shared platform state
that originates outside the engine's lock-free VM.Accept call-out —
  - block DECISION: block/executor Block.Accept/Reject hold &vm.stateLock
    (supplied to executor.NewManager) around the whole acceptor visit;
  - peer/lifecycle: VM.Disconnected and the onReady/onBootstrapStarted/Shutdown
    Start/StopTracking uptime flushes hold vm.stateLock.
This is avalanchego's ctx.Lock invariant (accept serialized with
engine.Connected/Disconnected), scoped to the state the platform VM owns and
implemented at the VM: the Lux engine invokes VM.Accept as a lock-free call-out
(no ctx.Lock over accept exists) and a chain-agnostic blockHandler cannot reach a
per-VM accept lock, so routing "through the engine" is not feasible.

CRITICAL #1 — C-Chain nil-version panic on state sync:
blockHandler.Connected passed connector.Connected(ctx, nodeID, nil). proposervm
promotes Connected to coreth, whose state-sync peer tracker compares peer
versions; a nil version deref panics any C-Chain node running state sync (a fresh
join OR a validator rejoining after falling behind — the launch's core invariant).
Fix: plumb the REAL peer version through a versionedConnector capability —
chainRouter.Connected converts the node peer version (luxfi/node/version) to the
VM boundary type (luxfi/version = chain.VersionInfo) and delivers it via
blockHandler.ConnectedWithVersion -> connector.Connected. Audit: geth in-process
Connected is a no-op stub (nil-safe); xvm stores the pointer without deref
(nil-safe); the real coreth plugin derefs -> fixed by feeding the real version.

Tests (SDKROOT + CGO_ENABLED=1, -race):
- vms/platformvm/uptime_state_race_test.go: state.Commit (VM.Disconnected path)
  concurrent with state.CommitBatch (acceptor path) under the shared lock — no
  race/fatal; verified meaningful (an unlocked probe trips DATA RACE).
- chains/blockhandler_connected_version_test.go + node/chain_router_connected_version_test.go:
  the real, converted version reaches the connector non-nil (dedup covered).
Uptime tracker 13/13 and the block/executor reward gate untouched and green;
go build ./... and go vet clean.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-24 00:34:00 -07:00
zeekayandHanzo Dev 88bb914cd6 fix(platformvm): accrue P-chain validator uptime for a stable set (reward gate)
All 5 mainnet validators read uptime=0.0000 / connected=null even for peers
connected 24h+, so prefersCommit (block/executor/options.go) always saw
0% < threshold and withheld staking rewards (~165M LUX gate).

Root causes (four, all fixed):

1. StartTracking never existed/called. The custom uptimeTracker was created
   late (onReady) and never baselined validator records, so a long-running
   validator's stored upDuration stayed 0 and CalculateUptimePercentFrom
   returned 0/total = 0.
2. upDuration flushed only on Disconnect/Shutdown, so a continuously-connected
   validator's persisted uptime never grew.
3. service.go left `connected` hard-nil ("IsConnected no longer exists").
4. ROOT: VM.Connected never fired. network.Connected -> chainRouter.Connected
   only added to a connectedPeers set and logged; it never dispatched to chain
   handlers, and blockHandler.Connected was a no-op. So tracker.Connect was
   never called and the connected map was always empty — nothing to accrue.

Fix (faithful port of avalanchego snow/uptime.Manager semantics, luxfi pkgs):

- vms/platformvm/uptime_tracker.go: rewrite as a startedTracking-gated tracker.
  StartTracking/StopTracking/StartedTracking/IsConnected added; CalculateUptime
  folds the live connected session forward to now using the persisted
  lastUpdated (reconstructed from the second-granular uptime.State encoding), so
  a connected validator accrues uptime WITHOUT a Disconnect. Before tracking, a
  validator is assumed online since its last update (avalanchego baseline);
  after tracking, only genuine sessions accrue. Second-granular clock matches
  storage. CalculateUptimePercentFrom is the clean upDuration/(now-from) form,
  clamped to [0,1].
- vms/platformvm/vm.go: create+register the tracker at Initialize (so bootstrap
  Connect events are captured), StartTracking(primary validators) at onReady,
  StopTracking on re-bootstrap and Shutdown. Mirrors avalanchego's
  onNormalOperationsStarted lifecycle.
- vms/platformvm/service.go: populate `connected` via tracker.IsConnected.
- node/chain_router.go: chainRouter.Connected/Disconnected now dispatch to every
  registered chain handler (snapshot under lock, call outside).
- chains/manager.go: blockHandler forwards Connected/Disconnected to its chain's
  VM (engineVM) exactly once (dedup set), so the P-chain uptime tracker — and
  every VM's peer set — finally observes connectivity.

Consensus safety: prefersCommit is a per-node PREFERENCE feeding oracle-block
voting, not a deterministic value. Fixing the local uptime measurement (which
was uniformly 0) changes only which reward option each node prefers; consensus
still converges by preference voting. No staking/reward math changed.

Tests (vms/platformvm/uptime_tracker_test.go, -race green):
- TestUptimeTrackerLongRunningValidatorAccruesUptime: a 30-day validator reports
  ~100%. Uses only the shared Calculator API; run against the OLD tracker it
  returns 0.0 (FAIL — the exact mainnet symptom), against the fix 1.0 (PASS).
- Continuously-connected-climbs, StartTracking-baselines, flush-on-disconnect,
  never-connected-gets-zero, StopTracking-flushes, idempotent double-connect,
  dedup, concurrency. block/executor (reward gate) suite green.

Follow-on (NOT in this commit): devnet 5-node uptime-climb verification ->
gated release -> owner-gated one-at-a-time mainnet roll. No image built, no
deploy, no live validator touched.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-23 21:44:08 -07:00
zeekayandHanzo Dev 4a3a131757 go.mod: consume consensus v1.36.9 + chains v1.7.9 (finality-halt fix + native-ZAP VMs)
Propagates two just-published upstream fixes into the node (luxd) binary:

- consensus v1.36.7 → v1.36.9: the chain engine no longer os.Exit(1)-kills a
  validator on a SetPreference orphan-refusal — it reconciles the VM to the
  certified block when the diverged tip is provably uncertified, and halts
  fail-closed only on a genuine double-finalization (v1.36.8). Also unifies
  consensus's transitive geth pin to v1.20.1, matching node/evm (v1.36.9). The
  new PreferenceReconciler VM interface is optional, so node's VMs are
  unaffected (they take the non-fatal defer path); no code change required here.
- chains v1.7.7 → v1.7.9: every VM (aivm/keyvm/quantumvm/dexvm) now serializes
  through native luxfi/zap struct-is-wire, with canonical-id hardening.

go.mod/go.sum only — dependency-graph resolution, no node code change. Transitive
indirects move forward within-major (bft, cockroachdb/errors, sentry-go,
prometheus/common, go-internal); no major-version bumps. geth stays v1.20.1
(already node's pin). Verified: GOWORK=off CGO_ENABLED=0 go build ./... clean.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-16 16:14:50 -07:00
zeekayandHanzo Dev 45a3dcfff1 fix(proposervm): millisecond-resolution block timestamps (unblocks sub-second cadence)
The proposervm block wire stored the timestamp as Unix SECONDS (timestamp.Unix()), so the
sub-second window/granularity work was silently truncated to whole seconds when written to the
block: buildChild computed the proposer slot from sub-second time, but verify read back
second-resolution and computed a DIFFERENT slot → errUnexpectedProposer verify-drops at any
window < 1s. Store timestamp.UnixMilli() and read time.UnixMilli() so sub-second timestamps
round-trip and build/verify agree on the slot. Measured at a 100ms window: errUnexpectedProposer
frequent→0, 108→149 TPS, 2.8s→2.2s cadence, 5/5 byte-identical. Forward-only wire change (the
outer proposervm timestamp; the inner EVM block timestamp is independent). Remaining sub-second
floor is now the EVM targetBlockRate, not the proposervm.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-16 09:00:21 -07:00
zeekayandHanzo Dev c3d6105804 feat(proposervm): configurable MinBlkDelay + timestamp granularity tracks WindowDuration (sub-second cadence foundation)
Two coupled knobs for differentiated cadence — co-located D-Chain/DEX (fast) vs public
C-Chain (standard):

1. MinBlkDelay was hardcoded to the 1s DefaultMinBlockDelay in chains/manager.go, ignoring
   its own --proposervm-min-block-delay flag. Now wired node.Config → ManagerConfig →
   proposervm.Config (0 ⇒ 1s default). High-throughput nets set it low.

2. Block timestamps were Truncate(time.Second) at 4 sites, quantizing cadence to 1s
   regardless of WindowDuration — so a sub-second window inflated slot numbers without finer
   time resolution and could NOT produce blocks faster than 1/s. Truncation now tracks
   proposer.TimestampGranularity() = min(1s, WindowDuration): mainnet (>=1s) keeps exact
   1-second block times; sub-second windows get matching sub-second timestamps.

Measured: 1s window/delay → ~95-104 TPS, byte-identical 5/5 finality (unchanged from before,
no regression). NOTE: true sub-second cadence additionally requires a slot-handoff fix — at
100ms the slot recomputed in buildChild from a drifted 'now' can differ from the slot
timeToBuild scheduled, causing errUnexpectedProposer verify drops; that + multi-sender
saturation is the next step. These knobs are the necessary foundation.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-15 22:33:01 -07:00
zeekayandHanzo Dev d83191e286 feat(proposervm): configurable window duration (fast local cadence) + strict-PQ classical-proposer refusal
Two changes, both node-local:

1. CADENCE — proposer.WindowDuration (the proposer-slot spacing, a validator's min build
   delay = slot index x WindowDuration) was a hardcoded 5s const tuned for mainnet-scale
   validator sets, flooring small/local block cadence at 5s per slot. It is now a
   startup-configurable var (default 5s, unchanged for mainnet) set via the new
   --proposervm-window-duration flag → node.Config → ManagerConfig → proposervm.Config →
   proposer.SetWindowDuration at VM init. Read by both the windower delay math and
   TimeToSlot, so they stay consistent. Measured on a 5-node strict-PQ net: 5s→1s took
   sustained C-Chain from 44 TPS / 14s-per-block to 104 TPS / 3.8s-per-block, still 5/5
   byte-identical finality.

2. SECURITY (cryptographer MEDIUM-1) — postForkCommonComponents.Verify now refuses a block
   carrying a CLASSICAL secp256k1 proposer identity when the chain is strict-PQ
   (StakingMLDSASigner set), UNCONDITIONALLY (before the consensusState==Ready gate, so it
   also holds during bootstrap/state-sync). Fills the documented-but-unwired 'proposer'
   SchemeGate site: the downgrade defense is now an explicit fail-closed in-perimeter gate,
   not merely emergent from 20-byte NodeID collision-resistance + upstream enforcement.
   Adds SignedBlock.HasClassicalProposer(). Mirrors contract.RefuseUnderStrictPQ.

Pins consensus v1.36.7 (consume-on-error build loop — kills the non-leader BuildBlock spin).
Block + proposervm VM tests green.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-15 22:24:36 -07:00
zeekayandHanzo Dev 13b6e80a78 fix(proposervm): strict-PQ proposer identity — sign+derive with ML-DSA-65 to match the windower
Under strict-PQ the canonical NodeID is ML-DSA-65-derived (config/node DeriveNodeID →
DeriveMLDSA), so the P-chain validator set and the proposervm windower are ML-DSA-keyed.
But proposervm signed post-fork blocks with the classical TLS leaf and derived the block
Proposer() via ids.NodeIDFromCert — a different value than DeriveMLDSA — so every signed
block (height ≥ 2) failed verifyPostDurangoBlockDelay with errUnexpectedProposer, was
dropped, and rebuilt in an unbounded storm (block 1 survived only because the unsigned
transition block skips the proposer comparison).

The block's offCert slot now carries a scheme-tagged proposer identity [scheme:1B|identity]:
0x90 classical (cert DER → NodeIDFromCert, ECDSA verify) or 0x42 strict-PQ (raw ML-DSA-65
pubkey → DeriveMLDSA(ids.Empty,pub), ML-DSA verify). ML-DSA signing/verification uses a
FIPS 204 §5.2 domain-separation context ('lux-proposervm-block-v1') so a proposer signature
can never be replayed as another ML-DSA message. proposervm.Config gains StakingMLDSASigner
/StakingMLDSAPub, plumbed from StakingConfig through ManagerConfig; exactly one scheme is
active per chain. K=1 nets are unaffected (transition + no-window blocks are unsigned).

Proven on a 5-node strict-PQ local net: sustained multi-block production, every block
built once, gossiped, and finalized byte-identical on all 5 (no automine). Pins consensus
v1.36.6 (engine logger + logged build-drops) which made this diagnosable.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-15 17:59:16 -07:00
zeekay 57792ee625 chore(deps): bump geth v1.20.1 + luxfi deps — stack unification 2026-07-15 11:20:41 -07:00
zeekayandHanzo Dev cf8e4c6f51 fix(chains): raise VM-startup timeout 30s→10m (vmStartupTimeout)
The 9 VM lifecycle ops (Initialize, Linearize, SetState, CreateHandlers,
router AddChain, state-sync) were each bounded at a hardcoded 30s. A cold
coreth 'Regenerate historical state' pass after an unclean shutdown takes
67-134s on the mainnet C-Chain, blowing that budget → context cancelled
mid-init → VM marked failed → C-Chain route never registered → the recurring
post-restart 404 that required a second restart to clear. One named bounded
constant (10m) covers regen with margin while still surfacing a truly-hung VM.
Stop stays 10s. This is the third stacked restart-fragility bug after
skip-bootstrap frontier (v1.36.11) and rejoin discriminator (v1.36.13).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-14 20:53:57 -07:00
zeekayandHanzo Dev 6ca0508608 v1.36.13: durable rejoin fix now fires for the C-Chain (discriminate on validating Net, not blockchain ID)
The #66/#74 durable rejoin fix was INERT for the C-Chain — the chain whose 40h
mainnet freeze motivated it. chains/manager.go gated expectsStakedBeacons on
ids.IsNativeChain(chainParams.ID) (the blockchain ID), but ids.IsNativeChain only
matches the symbolic 111...C alias; every deployed C/X/Q has a HASH blockchain ID
(devnet 21HieZng, mainnet 2wRdZG), so isNativeChain was ALWAYS false and under the
production --skip-bootstrap=true the beacons were emptied -> a behind C-Chain named
its stale local tip the frontier and never caught up. Verified on devnet v1.36.12:
C-Chain wedged at height 0 ('using empty beacons for single-node mode').

Fix: discriminate on the VALIDATING NET (chainParams.ChainID == PrimaryNetworkID)
via new chainValidatesOnPrimaryNetwork — PrimaryNetworkID for C/X/Q, the sovereign
net ID for L2s. C/X/Q now keep staked beacons under --skip-bootstrap (peer-sync a
behind validator); L2s keep the empty-beacon single-node path. New regression
TestChainValidatesOnPrimaryNetwork_RealHashChainID exercises the real discriminator
with hash IDs; TestRED_EmptyStakedSetFailsSafe still green (forged-frontier gate intact).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-14 18:51:43 -07:00
zeekayandHanzo Dev 35cbdfb0ee docs(LLM): v1.36.12 fleet rollout state — plugin-api blocker, codec migration, RewardManager runbook
Captures the devnet-canary findings so the gated rollout is resumable: the
v1.36.11 EVM-plugin api skew (fixed in v1.36.12), the v1.36.2->v1.36.x P-Chain
codec migration (one-time wipe + proven cross-version re-bootstrap), the durable
rejoin mechanism, per-net RewardManager addresses/config shape, and the
one-at-a-time verify-tip roll protocol. Also folds in the pre-existing
RewardManager->DAO-Safe C-Chain design note.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-14 18:23:55 -07:00
zeekayandHanzo Dev e266345e69 v1.36.12: align bundled EVM/dexvm plugins to node api v1.0.16 (fixes C/D-Chain init)
v1.36.11's node binary is correct (durable rejoin fix fe23efd1f2), but its image
baked VM plugins built against a stale luxfi/api: the C-Chain EVM plugin from
luxfi/evm@v1.104.8 and the D-Chain dexvm plugin from luxfi/dex@v1.5.15 both resolve
api v1.0.15, while the node pins api v1.0.16. api v1.0.16 APPENDED
InitializeResponse.Capabilities (uint64) for the Quasar-export handshake (api
1f2dc5a). The node decodes that field; the stale plugins never encode it, so
vms/rpcchainvm/zap/client.go fails every EVM VM Initialize with
'zap decode initialize response: unexpected EOF'. Native VMs (P/X/Q) are unaffected;
every EVM chain (C, D, and the L2 EVMs) fails to boot. Verified on devnet (published
v1.36.11 digest c3cf92a6): P/X re-bootstrap fine, C+D deterministically EOF.

Fix (image-only; node source unchanged beyond the version bump):
- EVM_VERSION v1.104.8 -> v1.104.9 (api v1.0.15 -> v1.0.16, indirect via luxfi/vm)
- force luxfi/api@v1.0.16 in the dexvm build stage (no dex release pins v1.0.16 yet)
- CHAINS_REF v1.7.6 already carries api v1.0.16 (the other 10 VMs were fine)
The api bump is code-free for plugins (chains v1.7.4->v1.7.5 adopted it go.mod-only).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-14 18:19:47 -07:00
zeekayandHanzo Dev 45bbe13637 node: purge dead protobuf tooling — ZAP-native, .proto codegen fully retired
Node has ZERO .proto files and ZERO .pb.go — the wire is hand-written ZAP schemas
(proto/{platformvm,vm,p2p,sync}/*_zap.go). The buf/protoc tooling around it was
orphaned: removed proto/{buf.yaml,buf.gen.yaml,Dockerfile.buf,buf.md,README.md},
scripts/protobuf_codegen.sh, the Taskfile generate-protobuf + check-generate-protobuf
targets, ci.yml buf-lint + check_generated_protobuf jobs (the latter ran the deleted
script → would fail CI), and the buf-lint.yml workflow. defaultPatch 10→11 (dev
version string; image already correct via ldflags). Binary unchanged — tooling/CI only.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-14 18:01:27 -07:00
zeekayandHanzo Dev fe23efd1f2 chains: skip-bootstrap must not disable peer-sync on a staked network (#66/#74)
Durable root-cause fix for the behind-validator rejoin wedge (mainnet luxd-0:
C-Chain frozen ~40h at a stale height, only a manual chaindata wipe unstuck it).

Root cause: buildChain computed the bootstrap frontier-sync discriminator as
`expectsStakedBeacons := !m.SkipBootstrap && native && !platform` AND emptied the
beacon set whenever `m.SkipBootstrap`. Production validators hardcode
--skip-bootstrap=true (to skip the initial bootstrap WAIT), so a real multi-
validator native chain (C/X/Q) got expectsStakedBeacons=false + EMPTY beacons.
FrontierTip then reported FrontierNoBeacons ("nothing to sync to"), the node named
its STALE local last-accepted the network frontier, transitioned the VM to normal
operation there, and never fetched the gap from its 4 healthy peers — the wedge
survived every restart because --skip-bootstrap is persistent config. The entire
peer-sync/self-heal machinery (bootstrap_sync.go) was dead code under skip-bootstrap.

Fix: drive the discriminator from SYBIL PROTECTION (the true "real staked network"
signal, already wired into ManagerConfig), not --skip-bootstrap. A sybil-protected
native non-platform chain now keeps its staked beacon set and expectsStakedBeacons
even under --skip-bootstrap, so a behind validator always catches up from peers.
A genuine single-node / dev net runs sybil-protection OFF and still takes the
empty-beacon immediate-start path. A single-VALIDATOR staked net (self-only set) is
handled by a new FrontierTip hasExternalBeacons rule (placed after the P-ready gate),
so it immediate-starts without a Connecting hang while a >=2 set runs the quorum.

This matches the proven live remediation (flipping skip-bootstrap=false via the
.allow-bootstrap marker caught luxd-0 up to tip in ~90s) and preserves every RED
safety invariant (empty staked set still fails safe — TestRED_EmptyStakedSetFailsSafe).

Tests: TestChainExpectsStakedBeacons_SybilDrivesNotSkipBootstrap,
TestNodeBootstrap_SelfOnlyStakedSet_ReportsNoBeacons, and the headline
TestNodeBootstrap_BehindValidator_StakedSet_CatchesUpNoWipe (N-blocks-behind →
restart → catches up from peers to tip, no wipe). Full chains suite green.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-14 16:14:50 -07:00
87 changed files with 5030 additions and 3977 deletions
-1
View File
@@ -1 +0,0 @@
# CI Status Check - 2025-09-23 23:30:58
-1
View File
@@ -1 +0,0 @@
# Triggering CI - Version 1.13.5 Ready
-34
View File
@@ -1,34 +0,0 @@
name: Lint proto files
on:
push:
paths:
- 'proto/**/*.proto'
- '.github/workflows/buf-lint.yml'
permissions:
contents: read
jobs:
buf-lint:
runs-on: lux-build-amd64
steps:
- uses: actions/checkout@v4
- name: Check for .proto files
id: proto-check
run: |
if find proto -name '*.proto' -type f 2>/dev/null | grep -q .; then
echo "has_proto=true" >> $GITHUB_OUTPUT
else
echo "has_proto=false" >> $GITHUB_OUTPUT
echo "No .proto files found — skipping buf-lint (node is ZAP-native by default; protobuf is opt-in)."
fi
- uses: bufbuild/buf-setup-action@v1
if: steps.proto-check.outputs.has_proto == 'true'
with:
github_token: ${{ github.token }}
version: "1.47.2"
- uses: bufbuild/buf-lint-action@v1
if: steps.proto-check.outputs.has_proto == 'true'
with:
input: "proto"
-47
View File
@@ -94,53 +94,6 @@ jobs:
- name: Run actionlint
shell: bash
run: scripts/actionlint.sh
buf-lint:
name: Protobuf Lint
runs-on: lux-build-amd64
steps:
- uses: actions/checkout@v4
- name: Install buf
shell: bash
run: |
BUF_VERSION="1.47.2"
curl -sSL "https://github.com/bufbuild/buf/releases/download/v${BUF_VERSION}/buf-Linux-x86_64" -o /usr/local/bin/buf
chmod +x /usr/local/bin/buf
buf --version
- name: Lint protobuf
shell: bash
run: buf lint proto
check_generated_protobuf:
name: Up-to-date protobuf
runs-on: lux-build-amd64
continue-on-error: true
env:
GOPRIVATE: github.com/luxfi/*
GONOSUMDB: github.com/luxfi/*
GOWORK: off
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-go-for-project
- name: Configure Git for private modules
shell: bash
run: git config --global url."https://${{ github.token }}@github.com/".insteadOf "https://github.com/"
- name: Install buf
shell: bash
run: |
BUF_VERSION="1.47.2"
curl -sSL "https://github.com/bufbuild/buf/releases/download/v${BUF_VERSION}/buf-Linux-x86_64" -o /usr/local/bin/buf
chmod +x /usr/local/bin/buf
- name: Install protoc-gen-go tools
shell: bash
run: |
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.35.1
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0
go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest
- shell: bash
run: scripts/protobuf_codegen.sh
env:
CGO_ENABLED: '0'
- shell: bash
run: .github/workflows/check-clean-branch.sh
check_mockgen:
name: Up-to-date mocks
runs-on: lux-build-amd64
+16
View File
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.36.31]
### Fixed
- **`/v1/health` reported a chain the node denies exists.** The `bootstrapped` check publishes `chains.Nets.Bootstrapping()` verbatim as its message, but `Nets.chains` is keyed by **net** ID and the aggregate appended the map **key**, not the chain. Every primary-network chain that failed to converge therefore surfaced as the single ID `11111111111111111111111111111111LpoYY``constants.PrimaryNetworkID` (`ids.Empty`) — so an operator resolving it got "there is no chain with alias/ID", and N stuck chains collapsed into one indistinguishable entry (measured on devnet/testnet: `"message":["11111111111111111111111111111111LpoYY"],"contiguousFailures":3213`). The net owns the bootstrapping set, so the net now names its own chains: new `nets.Net.Bootstrapping() []ids.ID` (`nets/net.go`), and `chains/chains.go` aggregates those instead of the keys. `TestNetsBootstrappingReportsChainsNotNets` asserts `ids.Empty` never appears and that two stuck chains are individually named; `TestNetsBootstrapping` no longer asserts the bug (it demanded the net ID).
- Ships the GET `/v1/health` encoder fix from `dada5a31` (`{"healthy":…,"error":"health reply encode failed"}` on every node — jsonv2 has no representation for `apihealth.Result.Duration`), which was on `main` but had never been tagged.
## [1.36.13]
### Fixed
- **The durable rejoin fix (#66/#74) shipped INERT for the C-Chain — the exact chain whose 40h mainnet freeze motivated it.** `chains/manager.go` gated `expectsStakedBeacons` on `ids.IsNativeChain(chainParams.ID)` — the *blockchain* ID. But `ids.IsNativeChain` only matches the symbolic `111…C` alias form, and every deployed C/X/Q carries a **hash** blockchain ID (devnet C `21HieZng…`, mainnet C `2wRdZG…`); only the P-chain has a symbolic blockchain ID (`111…P`), and it is excluded as the platform chain. So `isNativeChain` was **always false** for the real C-Chain → `expectsStakedBeacons=false` → under the production `--skip-bootstrap=true` the beacon set was emptied → a behind C-Chain named its STALE local tip the frontier and never caught up (verified on devnet v1.36.12: C-Chain wedged at height 0 with "using empty beacons for single-node mode"). Fix: discriminate on the **validating Net** (`chainParams.ChainID == constants.PrimaryNetworkID`) via new `chainValidatesOnPrimaryNetwork`, which is `PrimaryNetworkID` for C/X/Q and each sovereign L1's own net ID for an L2 — so C/X/Q now keep their staked beacons under `--skip-bootstrap` (peer-sync a behind validator) while L2s keep the empty-beacon single-node path. Regression test `TestChainValidatesOnPrimaryNetwork_RealHashChainID` exercises the real discriminator with hash chain IDs (the prior hardcoded-bool tests could not catch this); `TestRED_EmptyStakedSetFailsSafe` still passes (forged-frontier gate intact).
## [1.36.12]
### Fixed
- **EVM chains (C/D/L2) failed to initialize on v1.36.11 — bundled VM plugins were built against a stale `luxfi/api`.** The node pins `luxfi/api v1.0.16`, whose `InitializeResponse` carries the appended `Capabilities uint64` field (Quasar-export handshake, api commit `1f2dc5a`). The image baked the C-Chain EVM plugin from `luxfi/evm@v1.104.8` and the D-Chain dexvm plugin from `luxfi/dex@v1.5.15`, both of which resolve `luxfi/api v1.0.15` (no `Capabilities`). Their `InitializeResponse.Encode` writes a shorter payload than the node's strict `InitializeResponse.Decode` expects, so `vms/rpcchainvm/zap/client.go` fails every EVM VM handshake with `zap decode initialize response: unexpected EOF`. Native VMs (P/X/Q) were unaffected. Fix: bump `EVM_VERSION` `v1.104.8 → v1.104.9` (api v1.0.16) and force `luxfi/api@v1.0.16` in the dexvm build stage. The api bump is code-free for plugins (`luxfi/chains v1.7.4 → v1.7.5` adopted it with a go.mod-only diff; `CHAINS_REF=v1.7.6` already carries v1.0.16). No node source change beyond the version bump — this is v1.36.11's node binary (durable rejoin fix, commit `63f61429d1`) rebuilt with plugin↔node ZAP-wire alignment.
## [1.28.0]
### Added
+57 -19
View File
@@ -124,22 +124,27 @@ RUN --mount=type=secret,id=ghtok,required=false \
# linked into the C-Chain plugin via github.com/luxfi/chains/evm/cevm
# and become the default execution backend (parallel + GPU EVM).
#
# CI/RELEASE GAP: the luxcpp/cevm release artifacts MUST publish per-arch
# tarballs at the URL below for both linux-x86_64 and linux-arm64. Until
# those tarballs exist, builds with CGO_ENABLED=1 will fail at this step and
# operators must build with CGO_ENABLED=0 (pure-Go fallback).
# The release assets live in the PRIVATE lux-private/cevm repo, so the fetch is
# authenticated with the same `ghtok` secret as the private go modules and
# lux-accel above. Best-effort, like lux-accel: only a build that asks for the
# cevm backend (EVM_CGO=1 below) actually needs these.
ARG CGO_ENABLED=1
ARG CEVM_VERSION=v0.19.0
RUN if [ "${CGO_ENABLED}" = "1" ]; then \
ARG CEVM_VERSION=v0.51.10
ARG CEVM_REPO=lux-private/cevm
RUN --mount=type=secret,id=ghtok,required=false \
if [ "${CGO_ENABLED}" = "1" ]; then \
ARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) && \
if [ "$ARCH" = "amd64" ]; then CEVM_ARCH="linux-x86_64"; else CEVM_ARCH="linux-arm64"; fi && \
wget -q "https://github.com/luxcpp/cevm/releases/download/${CEVM_VERSION}/luxcpp-cevm-${CEVM_ARCH}.tar.gz" \
-O /tmp/cevm.tar.gz && \
tar -xzf /tmp/cevm.tar.gz -C /usr/local && \
rm /tmp/cevm.tar.gz && \
ldconfig 2>/dev/null || true ; \
AUTH=""; [ -s /run/secrets/ghtok ] && AUTH="--header=Authorization: Bearer $(cat /run/secrets/ghtok)"; \
( wget -q ${AUTH:+"$AUTH"} \
"https://github.com/${CEVM_REPO}/releases/download/${CEVM_VERSION}/luxcpp-cevm-${CEVM_ARCH}.tar.gz" \
-O /tmp/cevm.tar.gz \
&& tar -xzf /tmp/cevm.tar.gz -C /usr/local \
&& rm /tmp/cevm.tar.gz \
&& ldconfig 2>/dev/null \
) || echo "WARN: cevm ${CEVM_VERSION} fetch skipped (unreachable; needed only when EVM_CGO=1)" ; \
else \
echo "CGO_ENABLED=0: skipping luxcpp/cevm fetch (pure-Go fallback build)" ; \
echo "CGO_ENABLED=0: skipping cevm fetch (pure-Go fallback build)" ; \
fi
# Build node. CGO_ENABLED=1 (default) links luxcpp/cevm for parallel + GPU EVM.
@@ -257,7 +262,35 @@ RUN . ./build_env.sh && \
# failed ValidateState, and BRICKED the node. Proven on-node: real swap → kill -9 →
# clean reboot, state intact. v1.99.40 = v1.99.39 + deps to latest. consensus v1.25.21 =
# stake-weighted alpha-of-K quorum finality + per-height single-finalize + epoch-bound certs.
ARG EVM_VERSION=v1.104.8
# v1.104.9 bumps luxfi/api v1.0.15 -> v1.0.16 (indirect via luxfi/vm). v1.0.16
# APPENDED InitializeResponse.Capabilities (uint64) for the Quasar-export
# handshake (api commit 1f2dc5a). The node pins api v1.0.16 and DECODES that
# field; an EVM plugin built at v1.104.8 (api v1.0.15) omits it, so the node's
# strict struct decode hits "zap decode initialize response: unexpected EOF"
# and EVERY EVM chain (C + L2s hanzo/zoo/pars/spc, all mgj786) fails to
# initialize. The api bump is code-free for plugins (chains v1.7.4->v1.7.5
# adopted it with a go.mod-only diff), so v1.104.9 = v1.104.8 + api alignment.
# C-Chain execution backend. The default is the pure-Go EVM, which is what
# every image has shipped so far. EVM_CGO=1 EVM_TAGS=cevm links luxcpp/cevm
# instead — that pair is what makes AutoEVM resolve to CppEVM. The libraries
# come from the cevm fetch in this same stage above.
ARG EVM_CGO=0
ARG EVM_TAGS=""
# v1.104.22 realigns the plugin with THIS node's own go.mod: api v1.0.16 ->
# v1.1.1, vm v1.2.6 -> v1.3.1, geth v1.17.12 -> v1.20.1. Node main pins api
# v1.1.1, so pinning an evm at api v1.0.16 reintroduces the exact
# InitializeResponse decode mismatch described above, just in the opposite
# direction — keep this ARG and node's go.mod on the same api/vm/geth line.
#
# v1.104.14 is also the FIRST evm tag carrying the C-Chain fee-split seam
# (core/fee_split.go creditTxFee + extras.FeeSplitTimestamp/FeeRewardVault).
# Below it, encoding/json silently DISCARDS the genesis "feeSplitTimestamp"
# key: a chain configured for the 50/50 split instead routes 100% of every fee
# to the block coinbase and the reward vault 0x0100..0002 stays 0 forever, with
# nothing burned. The split stays dormant wherever feeSplitTimestamp is absent
# (mainnet), so this bump is behaviour-preserving there.
ARG EVM_VERSION=v1.104.23
ARG EVM_VM_ID=mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6
# the pinned evm go.mod may pin a dead luxfi/upgrade pseudo-version
# (v1.0.1-0.20260603055252-f51810805436 — commit pruned from origin). Heal it to
@@ -284,8 +317,8 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
go mod edit -require=github.com/luxfi/chains@v1.7.0 && \
find /tmp/evm -name go.sum -exec sed -i -E '/^github.com\/(luxfi|hanzoai)\//d' {} + && \
GOARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \
CGO_ENABLED=0 GOFLAGS=-mod=mod \
go build -ldflags="-s -w" -o /luxd/build/plugins/${EVM_VM_ID} ./plugin && \
CGO_ENABLED=${EVM_CGO} GOFLAGS=-mod=mod \
go build -ldflags="-s -w" -tags "${EVM_TAGS}" -o /luxd/build/plugins/${EVM_VM_ID} ./plugin && \
chmod +x /luxd/build/plugins/${EVM_VM_ID} && \
rm -rf /tmp/evm
@@ -302,7 +335,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
# oraclevm -> r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS
# quantumvm -> ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug
# relayvm -> sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz
# mpcvm -> tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t
# mpcvm -> qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS
# zkvm -> vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9
# MUST track node's go.mod luxfi/chains (the D-Chain dexvm + 10 VM plugins).
@@ -346,7 +379,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
( cd /tmp/chains/relayvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz ./cmd/plugin ) || echo "WARN: relayvm plugin build skipped" ; \
( cd /tmp/chains/mpcvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t ./cmd/plugin ) || echo "WARN: mpcvm plugin build skipped" ; \
-o /luxd/build/plugins/qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS ./cmd/plugin ) || echo "WARN: mpcvm plugin build skipped" ; \
( cd /tmp/chains/zkvm && CGO_ENABLED=0 GOFLAGS=-mod=mod go build -ldflags="-s -w" \
-o /luxd/build/plugins/vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9 ./cmd/plugin ) || echo "WARN: zkvm plugin build skipped" ; \
( chmod +x /luxd/build/plugins/* 2>/dev/null || true ) && \
@@ -359,7 +392,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS \
ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug \
sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz \
tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t \
qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS \
vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9 ; do \
test -s /luxd/build/plugins/$p \
|| { echo "FATAL: required chain-VM plugin $p missing/empty — its build failed above (see the matching WARN line); the runtime-stage hard COPY would otherwise fail cryptically. Surface & fix the real Go build error, or remove the plugin from BOTH the build list and the runtime COPY."; exit 1; } ; \
@@ -402,6 +435,11 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
git clone --depth 1 --branch ${DEX_REF} https://github.com/luxfi/dex.git /tmp/dex && \
find /tmp/dex -name go.sum -exec sed -i -E '/^github.com\/(luxfi|hanzoai)\//d' {} + && \
cd /tmp/dex && \
# Align the dexvm plugin's ZAP wire (luxfi/api) with the node. No dex release
# pins api v1.0.16 yet (all <=v1.5.20 carry v1.0.15), so force it here; the
# bump is code-free (adds InitializeResponse.Capabilities, capability-gated),
# so dexvm emits the field the node decodes -> no "unexpected EOF" on D-Chain.
go mod edit -require=github.com/luxfi/api@v1.0.16 && \
. /build/build_env.sh && \
GOARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \
CGO_ENABLED=0 GOFLAGS=-mod=mod \
@@ -464,7 +502,7 @@ COPY --from=builder \
/luxd/build/plugins/r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS \
/luxd/build/plugins/ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug \
/luxd/build/plugins/sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz \
/luxd/build/plugins/tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t \
/luxd/build/plugins/qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS \
/luxd/build/plugins/vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9 \
/luxd/build/plugins/
WORKDIR /luxd/build
-324
View File
@@ -1,324 +0,0 @@
# Lux Mainnet Launch Checklist
Node: luxfi/node v1.24.11
Consensus: Quasar (BLS+Corona, slashing, stake-weighted sampling)
EVM: GPU ecrecover, 18 precompiles
Genesis: networkID=1, startTime=2025-12-12T21:06:51Z (mainnet), networkID=2, startTime=2026-02-10T16:00:00Z (testnet)
Precompile constraint: all activations MUST be after 2025-12-25
---
## Infrastructure
| Environment | Cluster | DOKS ID | K8s Version | Namespace | Validators |
|-------------|---------|---------|-------------|-----------|------------|
| Testnet | do-sfo3-lux-test-k8s | `005ec3c4` | 1.35.1-do.0 | lux-testnet | 11 (target) |
| Rehearsal | do-sfo3-lux-dev-k8s | `0ff340e1` | 1.35.1-do.0 | lux-devnet | 21 (target) |
| Mainnet | do-sfo3-lux-k8s | `04c46df5` | 1.34.1-do.4 | lux-mainnet | 21 (target) |
Current state: lux-k8s runs 5 validators (v1.23.31) via LuxNetwork CRD. Testnet cluster (lux-test-k8s) has a 3-replica StatefulSet (v1.23.40).
Image: `ghcr.io/luxfi/node:v1.24.11` (built via CI/CD, linux/amd64+arm64)
Staking keys: KMS at `kms.lux.network`, project `lux-infra`, synced via KMSSecret CRD
Secrets: never in manifests, never in env files, never committed
Genesis configs: `/Users/z/work/lux/genesis/configs/{testnet,mainnet,devnet}/`
K8s manifests: `/Users/z/work/lux/universe/k8s/`
Profiles: `standard.json` (~100MB/node), `max.json` (~512MB/node)
Bootstrappers:
- Mainnet: 5 seeds (ports 9631) -- `209.38.118.46`, `209.38.174.69`, `24.144.69.101`, `134.199.187.56`, `143.198.246.173`
- Testnet: 2 seeds (ports 9641) -- `134.199.187.16`, `209.38.174.84`
Consensus parameters (mainnet): K=20, AlphaPreference=15, AlphaConfidence=15, Beta=20, ConcurrentPolls=4
Tokenomics: 10B total supply (9 decimals), min validator stake 1M LUX, min delegator stake 25K LUX, combined staking allowed (NFT+delegation), 80% uptime threshold
C-Chain: chainId=96369 (mainnet), 96368 (testnet), gasLimit=12M, targetBlockRate=2s, minBaseFee=25gwei
---
## Phase 1: Testnet (lux-test-k8s, networkID=2)
Target: validate all consensus, EVM, and staking behavior with K=11 validators.
### 1.1 Deployment
- [ ] Update LuxNetwork CRD in `universe/k8s/lux-k8s/validators/statefulset.yaml` (testnet section): `validators: 11`, `image.tag: v1.24.11`
- [ ] Update lux-test-k8s StatefulSet in `universe/k8s/lux-test-k8s/testnet/statefulset.yaml`: replicas=11, image=v1.24.11
- [ ] Generate 11 staking key pairs via `lux cli` and store in KMS (`lux-infra/testnet/staking/`)
- [ ] Add 9 new bootstrapper entries to `genesis/configs/testnet/bootstrappers.json` (currently 2)
- [ ] Apply `max.json` profile for testnet validators (512MB/node for stress testing headroom)
- [ ] Regenerate testnet genesis with 11 initial validators via `genesis` tool
- [ ] Verify all precompile activation timestamps are after 2025-12-25
- [ ] Deploy via PaaS (platform.hanzo.ai), not manual kubectl
- [ ] Verify all 11 pods reach Running state
- [ ] Verify all 11 nodes report healthy via `/v1/health/liveness`
### 1.2 Bootstrap and Connectivity
- [ ] Verify all 11 validators discover each other via P2P (check `info.peers` RPC, expect 10 peers per node)
- [ ] Verify staking port 9641 reachable between all pods (`luxd-{0..10}.luxd-headless.testnet.svc.cluster.local:9641`)
- [ ] Verify P-chain bootstraps and all validators appear in `platform.getCurrentValidators`
- [ ] Verify C-chain bootstraps and produces blocks
- [ ] Verify X-chain bootstraps and processes UTXO transactions
### 1.3 Quasar Consensus Verification
- [ ] Submit transactions, verify Quasar finalization with K=11
- [ ] Verify BLS aggregate signatures in block headers
- [ ] Verify Corona optimistic fast path activates when all 11 validators are online
- [ ] Measure finality latency (target: sub-second with Corona)
- [ ] Verify stake-weighted sampling: validators with more stake get polled proportionally
### 1.4 EVM Execution
- [ ] Deploy a test contract, call all standard opcodes
- [ ] Submit 100 sequential transactions, verify correct nonce ordering
- [ ] Verify `eth_call` and `eth_estimateGas` return correct results
- [ ] Verify block gas limit is 12M (from cchain.json config)
- [ ] Verify minBaseFee=25gwei is enforced
### 1.5 GPU ecrecover
- [ ] Verify GPU backend auto-detection: CUDA on Linux DOKS nodes, Metal on macOS
- [ ] Run ecrecover-heavy workload (1000 signature verifications per block)
- [ ] Compare ecrecover throughput: GPU vs CPU fallback
- [ ] Verify graceful fallback to CPU when GPU unavailable (set `--gpu-backend=cpu`)
### 1.6 Precompiles (all 18)
- [ ] Test each precompile individually via contract calls
- [ ] Verify DEX precompile (LP-9010 PoolManager): pool creation, swaps, flash loans
- [ ] Verify DEX router precompile (LP-9012): multi-hop routing
- [ ] Verify all precompile addresses are deterministic and match spec
- [ ] Verify precompile gas metering is correct (no underpriced or overpriced ops)
- [ ] Verify precompiles revert correctly on invalid input
### 1.7 Slashing
- [ ] Craft equivocation evidence: have a validator sign two different blocks at same height
- [ ] Submit equivocation proof to P-chain slashing precompile
- [ ] Verify slashed validator's stake is burned
- [ ] Verify slashed validator is removed from active set
- [ ] Verify honest validators are unaffected
### 1.8 Uptime and Rewards
- [ ] Stop 1 validator (scale pod to 0)
- [ ] Wait for reward period to elapse
- [ ] Verify stopped validator's uptime drops below 80%
- [ ] Verify rewards are withheld for the stopped validator
- [ ] Restart the validator, verify it re-bootstraps and resumes
- [ ] Verify validators with >80% uptime receive expected rewards
### 1.9 Stress Test
- [ ] Run stress test: maximum TPS with 1B gas blocks (increase gas limit temporarily)
- [ ] Measure sustained TPS over 1 hour (target: verify consensus is the bottleneck, not EVM)
- [ ] Monitor memory usage per node (should stay within `max.json` profile ~512MB)
- [ ] Monitor disk I/O and database growth rate
- [ ] Verify no consensus stalls under load
- [ ] Verify block production rate stays at targetBlockRate=2s
### 1.10 Validator Join/Leave
- [ ] Add a 12th validator via `platform.addPermissionlessValidator` (permissionless staking)
- [ ] Verify new validator bootstraps from existing state
- [ ] Verify new validator begins participating in consensus
- [ ] Remove a validator via unstaking (wait for stake period to end or use testnet short periods)
- [ ] Verify removed validator exits gracefully
- [ ] Verify remaining validators continue producing blocks
### 1.11 Formal Verification
- [ ] Run Lean proofs for Quasar consensus safety and liveness
- [ ] Run TLA+ model checker for consensus state machine
- [ ] Run Tamarin prover for BLS+Corona security properties
- [ ] Run Halmos for EVM precompile correctness (symbolic execution)
- [ ] All proofs pass with zero counterexamples
---
## Phase 2: Mainnet Rehearsal (lux-dev-k8s, networkID=3)
Target: full mainnet simulation with real parameters for 72 hours.
### 2.1 Deployment
- [ ] Update LuxNetwork CRD (devnet section): `validators: 21`, `image.tag: v1.24.11`
- [ ] Generate 21 staking key pairs, store in KMS (`lux-infra/devnet/staking/`)
- [ ] Use mainnet genesis parameters (networkID=3, but same tokenomics, same stake amounts)
- [ ] Apply `max.json` profile
- [ ] Deploy via PaaS
- [ ] Verify all 21 pods healthy
### 2.2 Real Staking Parameters
- [ ] Configure minimum validator stake: 1M LUX
- [ ] Configure minimum delegator stake: 25K LUX
- [ ] Configure max delegation ratio: 10x
- [ ] Configure NFT staking tiers (Genesis 500K/2x, Pioneer 750K/1.5x, Standard 1M/1x)
- [ ] Verify combined staking logic: NFT value + delegation + staked >= 1M
- [ ] Verify B-chain validators require 100M LUX + KYC
### 2.3 72-Hour Soak Test
- [ ] Start clock. Record block height and timestamp.
- [ ] Continuous transaction load: 50 TPS sustained
- [ ] Monitor: CPU, memory, disk, network per node (Prometheus + Grafana via PaaS)
- [ ] Monitor: consensus latency p50/p95/p99
- [ ] Monitor: block production rate (target: 1 block per 2s)
- [ ] Monitor: peer count stability (all 21 connected)
- [ ] Monitor: no OOMKills, no pod restarts, no crashloops
- [ ] At hour 24: rolling restart of 5 validators (verify zero downtime)
- [ ] At hour 48: simulate network partition (isolate 7 nodes), verify chain halts (< 2/3 online)
- [ ] Restore partition, verify chain resumes within 30s
- [ ] At hour 72: record final block height, calculate actual vs expected blocks
- [ ] Pass criteria: zero consensus faults, zero data loss, <1% block time variance
### 2.4 Security Audit
- [ ] External security audit firm engaged (Red team)
- [ ] Audit scope: consensus, EVM, precompiles, staking, slashing, P2P networking
- [ ] Audit result: 0 critical findings, 0 high findings
- [ ] All medium findings remediated or accepted with documented risk
- [ ] Audit report signed and archived
### 2.5 Bridge / Teleport (B-Chain + T-Chain)
- [ ] Deploy MPC threshold signing (5 nodes, threshold 3) in `lux-mpc` namespace
- [ ] Deploy bridge UI and API in `lux-bridge` namespace
- [ ] Verify CGGMP21 keygen: 5 parties generate shared key
- [ ] Verify threshold signing: 3-of-5 produces valid signature
- [ ] Test cross-chain transfer: lock on source chain, mint on Lux
- [ ] Test reverse: burn on Lux, unlock on source chain
- [ ] Verify MPC API at `mpc-api.lux.network` responds
- [ ] Verify bridge handles partial MPC node failure (2 down, 3 still sign)
### 2.6 DEX (D-Chain + Precompiles)
- [ ] Deploy DEX precompile PoolManager (LP-9010) -- already active from genesis
- [ ] Deploy DEX Router precompile (LP-9012) -- already active from genesis
- [ ] Deploy off-chain CLOB matching engine
- [ ] Create liquidity pool via precompile
- [ ] Execute swap via router precompile
- [ ] Verify AMM pricing matches expected curve
- [ ] Verify flash loan execution and repayment
- [ ] Test CLOB: place limit order, verify fill
- [ ] Verify DEX on lux.exchange frontend connects to devnet
---
## Phase 3: Mainnet Launch (lux-k8s, networkID=1)
Target: production network with real value.
### 3.1 Pre-launch
- [ ] All Phase 1 items passed
- [ ] All Phase 2 items passed
- [ ] Security audit sign-off received
- [ ] Formal verification suite green
- [ ] Legal review complete (terms of service, validator agreements)
- [ ] Incident response runbook written and tested
### 3.2 Genesis Ceremony
- [ ] Final genesis config reviewed: `genesis/configs/mainnet/genesis.json` (networkID=1)
- [ ] Genesis startTime confirmed: 2025-12-12T21:06:51Z
- [ ] Initial allocations verified (500M initial + unlock schedule)
- [ ] All 5 bootstrapper IPs confirmed reachable on port 9631
- [ ] Genesis hash computed and published to lux.network
- [ ] Genesis block signed by founding validators
### 3.3 Validator Onboarding
- [ ] Update LuxNetwork CRD (mainnet section): `validators: 21`, `image.tag: v1.24.11`
- [ ] Scale from 5 current validators to 21
- [ ] Generate 16 new staking key pairs in KMS (`lux-infra/mainnet/staking/`)
- [ ] Update bootstrappers.json with all 21 validator endpoints
- [ ] Deploy via PaaS with rolling update strategy
- [ ] Verify all 21 validators healthy and in consensus
- [ ] Publish validator onboarding guide for external operators
- [ ] Open permissionless staking after initial stabilization period
### 3.4 Public RPC Endpoints
- [ ] Deploy KrakenD API gateway in `lux-gateway` namespace
- [ ] Configure rate limiting per IP and per API key
- [ ] Configure Cloudflare DNS (proxied, full SSL):
- `api.lux.network` -> gateway (C-chain + P-chain + X-chain RPC)
- `ws.lux.network` -> gateway (WebSocket subscriptions)
- [ ] Verify `eth_chainId` returns `0x17871` (96369)
- [ ] Verify `net_version` returns `96369`
- [ ] Verify RPC endpoints handle 10K req/s without degradation
- [ ] Verify WebSocket subscriptions for `newHeads`, `logs`, `pendingTransactions`
### 3.5 Explorer Deployment
- [ ] Deploy explorer (luxfi/explorer) in `lux-explorer` namespace (already has manifests for 5 chains)
- [ ] Configure for C-chain (chainId 96369)
- [ ] Configure indexers for all active chains
- [ ] Configure Cloudflare DNS: `explore.lux.network`
- [ ] Verify block display, transaction search, contract verification
- [ ] Deploy exchange frontend: `lux.exchange`
### 3.6 Bridge Activation
- [ ] Deploy MPC production cluster (5 nodes, threshold 3)
- [ ] Generate production MPC keys (CGGMP21 keygen ceremony)
- [ ] Store MPC key shares in KMS (`lux-infra/mainnet/mpc/`)
- [ ] Deploy bridge contracts on supported chains (ETH, BNB, Polygon, Arbitrum, Base, Optimism)
- [ ] Deploy bridge UI at bridge domain
- [ ] Configure Cloudflare DNS
- [ ] Enable deposits (one chain at a time, small limits first)
- [ ] Monitor for 24h, then raise limits
### 3.7 Post-Launch Monitoring
- [ ] Prometheus + Grafana dashboards live (via PaaS o11y stack)
- [ ] Alerts configured:
- Validator down (any pod not Ready for >5min)
- Consensus stall (no new block for >30s)
- Peer count drop (any node <15 peers)
- Memory usage >80% of limit
- Disk usage >70%
- Error rate >1% on RPC endpoints
- [ ] On-call rotation established
- [ ] Runbook covers: validator restart, chain halt recovery, emergency upgrade, key rotation
---
## Port Reference
| Network | HTTP | Staking | Metrics |
|---------|------|---------|---------|
| Mainnet | 9630 | 9631 | 9090 |
| Testnet | 9640 | 9641 | 9090 |
| Devnet | 9650 | 9651 | 9090 |
## Chain IDs
| Chain | Mainnet | Testnet | Devnet |
|-------|---------|---------|--------|
| C-Chain | 96369 | 96368 | 96370 |
| Zoo EVM | 200200 | 200201 | 200202 |
| Hanzo EVM | 36963 | 36964 | 36964 |
| SPC EVM | 36911 | 36910 | 36912 |
| Pars EVM | 494949 | 7071 | 494951 |
## File References
| What | Path |
|------|------|
| Node source | `~/work/lux/node/` |
| Genesis configs | `~/work/lux/genesis/configs/{mainnet,testnet,devnet}/` |
| Chain configs | `~/work/lux/genesis/configs/chain-configs/` |
| K8s manifests | `~/work/lux/universe/k8s/` |
| Validator CRD | `~/work/lux/universe/k8s/lux-k8s/validators/statefulset.yaml` |
| Testnet StatefulSet | `~/work/lux/universe/k8s/lux-test-k8s/testnet/statefulset.yaml` |
| Node profiles | `~/work/lux/node/config/profiles/{standard,max}.json` |
| Tokenomics config | `~/work/lux/node/config/tokenomics.go` |
| GPU config | `~/work/lux/node/config/gpu.go` |
| Health/consensus params | `~/work/lux/node/config/health.go` |
| Network registry | `~/work/lux/universe/NETWORKS.yaml` |
+374
View File
@@ -158,6 +158,370 @@ charge less.
- Relay (R-Chain): `~/work/lux/relay/vm/feegate.go` (re-exported by `~/work/lux/chains/relayvm/`)
- Graph (G-Chain): `~/work/lux/chains/graphvm/feegate.go` (read-only; NoUserTxPolicy)
## C-Chain tx-fee routing — RewardManager to DAO Safe (P-Chain: NO CHANGE)
Owner tokenomics pivoted: **100% of C-Chain tx fees accrue to the chain's DAO Gov
Safe** via the existing `rewardmanager` precompile (C-Chain only). This needs **no
P-Chain change** — routing is `GetCoinbaseAt` → reward address on the C-Chain. See
`~/work/lux/evm/LLM.md` → "C-Chain Tx-Fee Routing — RewardManager". The P-Chain
`feeRewardPool` fold-in below is **NOT built** (design-only, superseded); no
`vms/platformvm/state` change was made.
<details><summary>Superseded design — 50/50 burn + P-Chain staking-reward fold (dormant option)</summary>
If the DAO ever chooses an in-protocol 50/50 split, the C-Chain half exists (dormant,
`FeeSplitTimestamp` gated off) and the P-Chain fold-in would be: system-triggered epoch
export of the C-Chain vault C→P → persisted `feeRewardPool` in `vms/platformvm/state`
(mirror the `accruedFees` singleton, upgrade-safe) → pro-rata payout at
`vms/platformvm/txs/executor/proposal_tx_executor.go` `rewardValidatorTx` (~line 285),
unified into `PotentialReward`, NO second mint → decrement `currentSupply` by the epoch
burn. Model R1 (move-not-mint), conservation-exact; R2 (burn+re-mint) rejected.
</details>
## v1.36.12 fleet rollout — durable rejoin fix + RewardManager→DAO Safe (IN PROGRESS 2026-07-15)
Rolling the durable rejoin fix (node `63f61429d1`) across all Lux nets, gated
devnet→testnet→mainnet, + activating RewardManager fees. Two hard facts were found
on the devnet canary that change the naive "swap image" plan:
**BLOCKER (fixed in v1.36.12): published `node:v1.36.11` (digest `c3cf92a6`) cannot
run ANY EVM chain.** Its baked VM plugins were built against a stale `luxfi/api`:
C-Chain EVM from `luxfi/evm@v1.104.8` and D-Chain dexvm from `luxfi/dex@v1.5.15`
both resolve `api v1.0.15`; the node pins `api v1.0.16`, which APPENDED
`InitializeResponse.Capabilities` (Quasar-export handshake, api `1f2dc5a`). Node
decodes the field, stale plugins never encode it → `vms/rpcchainvm/zap/client.go`
fails every EVM `Initialize` with `zap decode initialize response: unexpected EOF`.
Native VMs (P/X/Q) unaffected. **Fix = v1.36.12** (this repo, tag pushed, ARC docker
build run 29381442539): `EVM_VERSION v1.104.8→v1.104.9`, force `api@v1.0.16` in the
dexvm build stage; `CHAINS_REF=v1.7.6` was already v1.0.16. Node binary unchanged
(still the durable fix). api bump is code-free for plugins (`chains v1.7.4→v1.7.5`
adopted it go.mod-only). **Roll v1.36.12, NOT v1.36.11.** (Also: `api 1f2dc5a` added
`Capabilities` WITHOUT bumping `version.RPCChainVMProtocol` (42) → skew is invisible
at handshake, only fails at Initialize decode. Consider bumping the protocol next
api-wire change so skews fail fast.)
**MIGRATION: v1.36.2→v1.36.x is a P-Chain codec change (linearcodec→ZAP-native),
one-time DB wipe + re-bootstrap.** v1.36.11/12 cannot read a v1.36.2 P-Chain zapdb
(`loadMetadata: feeState: zap: invalid magic bytes`; `state_commit.go:116` "database
must be wiped"). Recovery = wipe `/data/db`+`/data/chainData`, re-bootstrap from
peers. **Cross-version bootstrap (v1.36.11 node ← v1.36.2 peers) is PROVEN working**
(devnet luxd-1: P/X re-bootstrapped from the 4 v1.36.2 peers). Devnet startup got a
marker-gated one-time wipe (`/data/.zap-native-migrated`): absent→wipe+set marker,
present→NO wipe (the durable-fix no-wipe restart path). mainnet/testnet/zoo use
`startup.sh` which already has `.wipe-cchain` (C-Chain only) + `.allow-bootstrap`
(flips skip-bootstrap=false + EVM state-sync); for the codec migration the P-Chain
zapdb (`/data/db`) must also be cleared. **Mainnet C-Chain is 1.08M blocks → MUST
enable EVM state-sync for the re-sync (full replay is too slow); native VMs are tiny.**
**Durable fix (`63f61429d1`):** discriminator for keeping the staked beacon set is
SYBIL-PROTECTION, not `--skip-bootstrap`. So a behind validator on a sybil-protected
net catches up from peers even with `--skip-bootstrap=true` (which prod hardcodes),
no wipe. Devnet added `--skip-bootstrap=true` to the inline cmd to exercise this.
**RewardManager (C-Chain precompile, per-net `cchain-upgrade.json` → append one
`precompileUpgrades` entry, dated `blockTimestamp`):** proven testnet shape is
`{"rewardManagerConfig":{"blockTimestamp":<ts>,"adminAddresses":["<admin>"],
"initialRewardConfig":{"rewardAddress":"<reward>"}}}`. Reward addr = coinbase; 100%
fees land there, blackhole `0x0100…00` goes flat. Addresses: **testnet ALREADY LIVE**
(reward `0xEAbCC110fAcBfebabC66Ad6f9E7B67288e720B59`, admin `0x9011…94714`); **mainnet**
reward+admin = DAO Gov Safe `0x8E29b816c6C35b13cE1ff68D33E245C2bda8ac3D`; **zoo**
reward `0x229599f227231d8C90fcF1a78589F5DC4b7A6962`; **devnet** reward
`0x8d5081153aE1cfb41f5c932fe0b6Beb7E159cF84` (idx2), admin `0x9011…94714` (idx0).
Source ConfigMaps: devnet `luxd-chain-upgrades/cchain-upgrade.json`; mainnet+testnet
`luxd-startup/cchain-upgrade.json`; zoo `zood-mv-genesis/upgrade.json` (`--upgrade-file`).
**Rollout levers (lux-operator is scaled 0/0 — sts/cm are the live source of truth;
CRs are STALE, do not scale operator up mid-roll):** ports devnet 9650 / testnet 9640
/ mainnet 9630 / zoo 9630; RPC path `/v1/bc/C/rpc`; container `luxd` (`zood` on zoo).
Devnet uses an inline generated cmd; testnet/mainnet/zoo use `/scripts/startup.sh`.
**Zoo `zood-mv` trap: RollingUpdate + hardcoded `--skip-bootstrap=true` with NO
`.allow-bootstrap` gate → switch to OnDelete BEFORE rolling.** Lux sts are OnDelete.
Master funded key = LUX_MNEMONIC (secret `lux-deployer`) idx0 `0x9011…94714`;
`genesis/cmd/derivekey -mnemonic "$M"` (path m/44'/9000'/0'/0/i, `CGO_ENABLED=0`).
**Per-node roll protocol (ALL nets, one at a time, NEVER 2 mainnet down — quorum
4/5):** set sts image v1.36.12 (+ rewardManager cm edit) → delete ONE pod → WAIT
until it is back at **TIP HEIGHT matching the others** (NOT pod-Ready; a wedged node
false-reports Ready) AND C-Chain serves RPC → only then the next. If any node fails
to return to tip, STOP that net and report.
**State at pause:** v1.36.12 tag pushed + ARC build dispatched (run 29381442539).
Devnet sts = v1.36.11 + skip-bootstrap + wipe-marker; luxd-1 migrated (P/X up on
v1.36.11, C-Chain down = the plugin bug → will heal on v1.36.12); luxd-0/2/3/4 still
v1.36.2 healthy (devnet C-Chain 4/5). Nothing rolled on testnet/mainnet/zoo. NEXT:
when v1.36.12 image is ready → set devnet sts image v1.36.12, delete luxd-1, confirm
C-Chain inits + reaches tip; then finish devnet (durable-fix proof + RewardManager),
then gated testnet→mainnet→zoo.
## Crash-boot recovery armor + upstream delta verdict (vms/proposervm, test-only, 2026-07-28)
**Upstream review (avalanchego master `bcc851822d..c5d3c8aafe`, 6 commits): ZERO code
ports.** grpc bump (no named bug; would cascade through luxfi/vm), README typo, typed
sync-client scaffolding (client-side only, no serving handlers even upstream; our sync
direction is ZAP-native — reference for task #66 only), a Firewood `debug_intermediateRoots`
enabler (our tracers API is `.disabled`, twice N/A), and two streaming-VM-only mempool
guards whose failure modes we already bracket (`miner/worker.go:62 targetTxsSize=1800KiB`
build cap + txpool `ErrGasLimit`/128KB caps under the 2MiB wire cap). **Nothing in the
delta touches proposervm — the open `vm.go:380` preferred-fetch failure gets no upstream
help.** One upstream BRANCH does touch proposervm — `containerman17/proposervm-dedup-capability`
(+72 vm.go): inner-bytes dedup in the block store plus an extra boot-repair arm for the
unclean-shutdown window where the outer index lands above the inner survivor (the very
window `OuterCommittedInnerNot` below pins) — storage-layer work, not the build-path
preferred fetch, so it changes nothing about `vm.go:380` either. What WAS worth taking is
their recovery-test discipline, ported as:
**`vm_crashboot_test.go`** — copy the COMMITTED bytes out from under a RUNNING proposervm
(no Shutdown), boot a SECOND, cold VM over the copy (repair + metadata, Initialize order),
assert source-equality (finality pointer, fork height, every height's envelope openable
cold) and then BUILD on the recovered tip (outer parent == recovered envelope, inner
parent == its inner block — the errInnerParentMismatch invariant from a cold boot).
Matrix: nothing-accepted / first-accept / longer run / copy-while-source-runs-on, plus
`OuterCommittedInnerNot`: the one crash window the accept path leaves open
(`acceptPostForkBlock` commits BEFORE the inner accept) must boot via the roll-back arm
and re-propose. Harness seams added in height_lag_repro_test.go (`testVMOnBase`,
`acceptRangeThroughProposervm`). **Negative control executed**: deleting the
`vm.db.Commit()` from `acceptPostForkBlock` fails every persistence-bearing scenario
(4 of 6) — the armor bites on flush-order regressions, not just on green paths.
## v1.36.35 — the certified-descendant false halt + the plugin-killing map fatal (devnet 96367, 2026-07-28)
> Shipped as v1.36.35, not v1.36.34: the v1.36.34 tag inherited a build break that
> `011e9bf99d` ("deps: drop go.sum lines that disagree with the checksum log") had
> already pushed to main — it bumped `zap-proto/http` from a pseudo-version to v0.3.0,
> whose `Server.Handler` is a `fasthttp.RequestHandler` and whose `NewTransport` is now
> `Dial(network, addr)`, so `server/http/zap_listener.go` no longer compiled. **main was
> unbuildable from that commit until this one.** Repaired here by bridging the one
> net/http handler chain with `fasthttpadaptor.NewFastHTTPHandler` (the two transports
> stay behaviourally identical — only the wire encoding differs) and moving the
> round-trip test onto the fasthttp request/response pair. v1.36.34 produced no image
> and its tag was deleted.
Rolling devnet to v1.36.33 fixed the build→verify-fail→drop loop and unmasked two
DIFFERENT failures. Both are fixed here; each has a regression test that fails before
and passes after.
**P0-1 — five validators `os.Exit(1)` on a benign state.** Each devnet node hit, once:
```
error VM accepted head is CONSENSUS-CERTIFIED and conflicts with the newly finalized
block — refusing to orphan it orphanedHeight=1364
fatal SetPreference would orphan a CONSENSUS-CERTIFIED block — refusing (fail-closed)
error="cannot orphan finalized block at height: 1364 to common block at height: 1363"
```
at three distinct height pairs (1258/1259, 1363/1364, 1364/1365), ALWAYS with
`certified = head 1`, with every surviving node holding byte-identical blocks at all
five heights — no fork anywhere. (The "1168/1169" pair in the original report never
appears in any fatal line; those two blocks are a normal parent/child present on all
live nodes.)
Two defects in `luxfi/consensus`, one crash — fixed in **consensus v1.36.12**:
- *Producer.* `acceptWithCertCore` releases `t.mu` across every VM call-out, then steers
the VM with its STALE local `blockID`. A finalize that completed in that window has
already advanced the ledger AND the EVM to `blockID+1`, so the steer is BACKWARDS and
the EVM's accepted-irreversibility guard (`evm/core/blockchain.go:1987`,
`commonBlock < lastAccepted`) correctly refuses it. **That guard is right and is
untouched.** Now steers at the live build anchor (`PreferredBuildTip`
`ledger.BuildAnchor`), which the accept ordering (ApplyCert BEFORE VM.Accept) keeps at
or above the VM's own accepted head. Same value the build path already uses.
- *Classifier.* `reconcileVMToCertified` asked only "is the head the ledger's certified
canonical at ITS OWN height?" — trivially true for every healthy node whose head is
certified — and called that a two-blocks-at-one-height double-finalization. It never
established that `certified` was at that same height. The ledger holds one canonical
per height along one contiguous chain, so when both are certified at their own heights
they lie on that one chain and the head merely DESCENDS from the target: nothing is
orphaned. **The fail-closed halt is not weakened** — it now fires on the state that is
actually unsafe (steering off a certified head onto a block our own ledger does not
certify at its height).
Direction: NEITHER roll the head back NOR certify forward. The VM head is legitimately
ahead and already CONTAINS the certified block; `FinalityLedger.BuildAnchor` already
documents `head > certified` as the designed state, and rolling back is exactly what
`blockchain.go:1987` exists to refuse. The correct action is no action — plus not issuing
the backwards steer at all.
**P0-2 — the EVM plugin process dies and never comes back.** devnet luxd-1:
```
fatal error: concurrent map read and map write
vm/components/chain.(*State).getCachedBlock state.go:216
vm/components/chain.(*State).ParseBlock state.go:267
evm/plugin/evm.(*VM).ParseBlock vm.go:340
vm/rpc.(*zapVMServer).handleParseBlock vm_server_zap.go:477
```
`chain.State` was written against avalanchego's contract that the consensus engine holds
the chain lock across every VM call. The ZAP VM server does NOT reinstate it — it
dispatches ParseBlock/GetBlock and the Verify/Accept/Reject wrappers concurrently.
`verifiedBlocks` (plain map) and `lastAcceptedBlock` (pointer) are the only State that is
not self-synchronising; every `cache.Cacher` carries its own mutex. Fixed in **vm v1.3.3**
by giving State one RWMutex for exactly those two, never held across a call into the
inner VM. A Go map fatal is unrecoverable: it kills the plugin, **luxd survives and keeps
answering `info.getNodeVersion` while its chain is gone** — pod-Ready and `/v1/health`
both stay green — and there is no self-heal.
**Devnet roll result (10:2310:45Z).** All five pods on v1.36.35, `restarts=0` on every one
for 20+ minutes. Both fixed defects are GONE fleet-wide: `CONSENSUS-CERTIFIED` fatal = 0
(previously all five died within minutes), `concurrent map` = 0. luxd-1, whose C-Chain had
been dead ~45 min with no self-heal, came back at tip parity on first boot; luxd-3, stuck at
1524, caught up immediately. A transaction of ours got a receipt with identical status,
blockHash and resulting balance on all five nodes (`0xcb5e9a06…`, block 1636, status 0x1).
**NOT accepted — a THIRD, pre-existing defect blocks five-way parity.** Two nodes (luxd-2 at
1789, luxd-4 at 1825) freeze their tip while 0/1/3 advance in lockstep, emitting
```
error unexpected build block failure error="not found"
reason="failed to fetch preferred block; no distinct last-accepted fallback"
```
from `vms/proposervm/vm.go:380`. That branch is reached when the preferred block is
unfetchable AND last-accepted is either unreadable or the same id — a local storage/index
condition, not a steering choice. It is **NOT a regression from this release**: the identical
line appears on binaries built long before it — mainnet luxd-1 on **v1.36.2** (2,223
occurrences, and that is the one mainnet node still producing) and testnet luxd-3 on
**v1.36.24** (35,693). It is also survivable: the stalled node keeps VOTING, so quorum holds
and the chain keeps finalizing (devnet reached 1913 with two nodes frozen), and devnet luxd-1
self-recovered from it once (1609 → 1634). Left unfixed and unchained-to — it needs its own
diagnosis at the proposervm layer.
**Quorum arithmetic (reported, not changed).** Devnet and testnet both run
`--consensus-sample-size=5 --consensus-quorum-size=4`. With only 3 live C-Chains the
quorum is arithmetically unreachable; devnet demonstrated both directions in one session
(pinned at 1378 on 3 live, 1378→1395→1396 the moment luxd-4 restored a 4th). This is a
config value, not a code defect, and lowering it lowers the safety margin — left as is.
### Testnet 96368 roll (15:3216:00Z) — accepted, after two blockers the devnet roll never hit
Testnet was frozen at **1779** with only three live C-Chains, below `--consensus-quorum-size=4`
(`ceil(2·5/3)+1`), so no block could be accepted. luxd-1 and luxd-4 had no C-Chain at all —
`/v1/bc/C/rpc` 404 — on `failed to repair accepted chain by height: proposervm finality index
(height 1453 / 1463) is BEHIND the inner VM tip (height 1491)`. v1.36.35 turns that fatal into
a repair, and it worked on the first boot of each: `proposervm finality index REBUILT from the
local block store — index and inner tip agree fromHeight=1453 toHeight=1491`. The freeze broke
the instant a **fourth** C-Chain came up. Final: five nodes on v1.36.35, every binary
self-reporting `luxd/1.36.35`, `1779 → 1888` and climbing, tips in exact agreement, and one
real transaction (`0x333d5bbd…`, block `0x728`) returning `status=0x1` with **identical
blockHash `0x07fd0c68…` and `gasUsed=0x5208` on all five nodes**. α was NOT lowered.
Two defects had to be fixed first. Both are invisible on devnet and both apply to any fleet.
**1. The RLP startup import is fatal on re-run — it kills the C-Chain on EVERY boot.**
Testnet's startup script passes `--import-chain-data` on every boot, relying on
`isNothingToImportError` to make re-importing an already-imported chain a no-op. That guard
only ever existed on `luxfi/evm` `hotfix/v1.104.9` (commit `c58d307e`, tags
`v1.104.9-hotfix.2/3/4`); `git merge-base --is-ancestor c58d307e main` = **false**. Only the
*other* half of that commit was forward-ported. So images built from evm main die with
`startup import failed: no blocks imported (parsed=0)`. Proven from the two plugin binaries
in-cluster, with a positive control:
| string in `plugins/mgj786NP7…` | v1.36.24 | v1.36.35 |
|---|---|---|
| `ImportChain: resuming from current head` | 1 | 1 ← control |
| `nothing to import` | 1 | **0** ← the guard |
| `no blocks imported (parsed=` | 1 | 1 |
Fixed **twice, on purpose**: the guard is restored in `luxfi/evm` main (with
`startup_import_idempotency_test.go` locking the contract), and the flag is now gated on a
per-PVC sentinel in `universe/k8s/lux-testnet/luxd-startup.yaml` — a completed one-time
migration must not re-run forever. Sentinel pre-seeded on all five PVCs before the ConfigMap
was patched. **Mainnet is NOT exposed**: its `luxd-startup` ConfigMap (39,719 bytes, contains
`consensus-quorum-size` twice as a control) has zero `--import-chain-data` and no `.rlp` on disk.
**2. 🚨 The `luxfi/vm` map-race fix never reached the C-Chain.** node v1.36.35 bumped
`luxfi/vm` to v1.3.3 for it, but the C-Chain is a **plugin built from `luxfi/evm`**, which
still pinned **v1.3.1**. Read off the two binaries inside one v1.36.35 pod:
```
/luxd/build/luxd github.com/luxfi/vm@v1.3.3
/luxd/build/plugins/mgj786NP7… github.com/luxfi/vm@v1.3.1 ← verifies the blocks
```
It fired on testnet luxd-0 five minutes after the roll: `fatal error: concurrent map writes`
in `luxfi/vm@v1.3.1/components/chain/block.go:44 (*BlockWrapper).Verify` via
`rpc/vm_server_zap.go:580 handleBlockVerify`. v1.3.3 is precisely the fix for that line — it
takes `state.blocksLock` around `verifiedBlocks[blkID] = bw`, which v1.3.1 wrote unlocked from
every concurrent ZAP RPC handler. `luxfi/evm` main is now on v1.3.3; **the next node image
must be built after that bump, or this race ships again.**
⚠️ **The readiness probe cannot see this.** The plugin dies while luxd survives, so the pod
stays `ready=true`, `restarts=0`, and `info.isBootstrapped(C)` keeps answering `true` while
`eth_blockNumber` times out — a dead C-Chain still in the Service, the exact failure the probe
was redesigned to catch. Only a per-node **tip** probe sees it. Recovery is a pod delete.
## v1.36.33 — the build→self-verify-fail→drop loop (devnet 96367 / testnet 96368, 2026-07-28)
**Symptom.** Every proposer built a block and then rejected the block it had just
built, forever: `built block … height=1047` immediately followed by
`built block failed verification — dropping error="inner parentID didn't match
expected parent"`, 83456 drops/min per node, accepted tip frozen two heights BELOW
what the builder kept proposing (devnet tip 1045, builds 1047).
**Root cause (one line).** `buildChild` asked the inner VM for a block without first
pointing the inner VM at the parent's inner block. The inner VM builds on ITS OWN
head (`luxfi/evm` miner reads `bc.CurrentBlock()`); the verify path requires
`child.innerBlk.Parent() == parent.innerBlk.ID()`. Two different pointers, one
required equal to the other, nothing asserting it. The head drifts on its own:
verifying a GOSSIPED block whose parent is the current head optimistically sets the
head (`core/blockchain.go writeBlockAndSetHead → newTip → writeCanonicalBlockWithLogs`),
and `VM.SetPreference` short-circuits on an unchanged outer preference so it never
re-pushes the inner preference to drag the head back. Non-self-correcting by
construction.
**Fix.** `VM.anchorInnerBuildParent` (vms/proposervm/vm.go) — one inner
`SetPreference` at the point of use, called from BOTH build delegations
(`postForkCommonComponents.buildChild`, `preForkBlock.buildChild`). On a healthy node
the inner `setPreference` early-returns on `current.Hash() == block.Hash()`, so it is
a lookup and no state change; when it fails, the head is provably not the parent's
inner block, so refusing to build beats emitting a block we would drop.
Regression proof: `vms/proposervm/build_inner_parent_test.go` (models the three evm
head semantics — build-on-head, verify-advances-head, SetPreference-reorgs-head).
**NOT the cause, measured:** the P-Chain. `info.isBootstrapped{"chain":"P"}` = true on
15/15 nodes and `platform.getHeight` = 0 on 15/15 **including mainnet**, whose built
blocks also carry `pChainHeight=0`. `bootstrapped.message:["111…LpoYY"]` is the
primary-network NET id (`chains/chains.go Nets.Bootstrapping` keys by net), not the
P-Chain — the P-Chain's chain id prints `111…P` (`constants.PlatformChainID =
ids.PChainID`, while `PrimaryNetworkID = ids.Empty`). The verify path DOES read
pChainHeight before the parent check, but only as monotonicity
(`childPChainHeight < parentPChainHeight`, and 0 < 0 is false, so it passes); every
P-Chain-DEPENDENT validation — epoch, `GetCurrentHeight`, proposer window — is gated
behind `consensusState == Ready` and sits AFTER the parent check. So `pChainHeight=0`
cannot produce `errInnerParentMismatch`.
**Sibling failure modes on the same fleets (already fixed in 1.36.32, needs the roll):**
outer index BEHIND the inner tip ⇒ `refusing to build`/boot repair
(`height_backfill.go`, 7d2f01eb), and preferred-absent-locally ⇒ last-accepted
fallback (`vm.go BuildBlock`). All three are the same invariant seen from three sides.
**Roll surface — measured 2026-07-28T07:5xZ, do not use the LuxNetwork CR.**
`lux-operator` and `lux-operator-devnet` are **0/0** in `lux-system`, so nothing
reconciles `luxnetworks.lux.cloud/luxd`; its tags are stale garbage (mainnet
`v1.34.0`, testnet `v1.32.12` — v1.34.0 exists in no registry). The live image is on
the **StatefulSet**, hand-maintained: mainnet `ghcr.io/luxfi/node:v1.36.2`
(`kubectl-patch` 07-25T00:27:04Z), devnet `v1.36.25@sha256:ca497eff…`, testnet
`v1.36.24@sha256:91e2542b…`, all three `updateStrategy: OnDelete`. Change the image in
the universe manifest and apply, then delete one pod. Editing the CR and deleting a pod
reboots it on the OLD image.
**Mainnet 96369 is NOT a clean control — 3 of 5 nodes have Mode B, invisibly.**
luxd-0/3/4 `eth_blockNumber` = `0x10c1cf` (1098191, block ts 2026-07-24T15:46:19Z) and
have not moved in 3+ days while luxd-1 mines (1098341, tx status 0x1, 07:58Z); luxd-2
serves 404 (no C-Chain). Not a fork — 1098191 hashes identically on luxd-0 and luxd-1.
3998 of luxd-0's last 4000 log lines are the same `built block … height=1098196`. The
drop line is absent because the **binary** lacks it: `grep -c "built block failed
verification" /luxd/build/luxd` = **0** on mainnet v1.36.2, **1** on devnet v1.36.25.
`/v1/health` says `{"healthy":true,"error":"health reply encode failed"}` on the frozen
node and the mining node alike — never gate on it, use tip parity.
Two hazards for the roll: the 4 broken mainnet pods are exactly the ones on
ControllerRevision `luxd-596857c9d6` (rev 147, kubectl-patch 07-25: GOMEMLIMIT=6GiB,
mem limit 16Gi→12Gi, request 4Gi→7Gi) and the one mining node, luxd-1, is the only pod
still on rev 144 — **deleting luxd-1 recreates it on the revision every other node
broke on**. And `ghcr.io/luxfi/node:v1.36.2` has no git tag at all
(`git ls-remote origin refs/tags/v1.36.2` → empty; v1.36.24/25 → present), so what
mainnet runs is not reproducible from this repo.
## Essential Commands
### Release & build (canonical) — via platform.hanzo.ai, NOT GitHub Actions
@@ -840,4 +1204,14 @@ v2 semantic differences worth knowing (these change wire shape):
---
## Housekeeping
Removed 6 generated write-ups / stale root artifacts (`LAUNCH_CHECKLIST.md`,
`rename_app.sh`, `replace_imports.sh`, `gen_zoo_addr` binary, `.ci-status-check.md`,
`.ci-trigger`) plus the 73MB `.claude/worktrees/` agent scratch tree. Release and
launch state live in this file, `CHANGELOG.md`, `RELEASE.md`; chain IDs/ports in
`~/work/lux/universe/NETWORKS.yaml` and `~/work/lux/genesis/configs/`.
---
*Last Updated*: 2026-06-06
+8 -1
View File
@@ -7,8 +7,15 @@ CGO_ENABLED ?= 1
FIPS_STRICT ?= 0
# Go 1.26 experimental features:
# runtimesecret - zeroes stack/register state after secret.Do() for forward secrecy
# runtimesecret - zeroes stack/register state after secret.Do() for forward secrecy.
# It SIGSEGVs at startup under the WSL2 kernel (confirmed on go1.26.3 and go1.26.4), so enable
# it only off-WSL; forward secrecy stays on for real Linux/macOS/production builds.
WSL := $(shell grep -qiE 'microsoft|WSL' /proc/sys/kernel/osrelease 2>/dev/null && echo 1)
ifeq ($(WSL),1)
GOEXPERIMENT ?= none
else
GOEXPERIMENT ?= runtimesecret
endif
export GOEXPERIMENT
# FIPS 140-3 always enabled (required for blockchain/financial systems)
-10
View File
@@ -73,12 +73,6 @@ tasks:
- task: generate-mocks
- task: check-clean-branch
check-generate-protobuf:
desc: Checks that generated protobuf is up-to-date (requires a clean git working tree)
cmds:
- task: generate-protobuf
- task: check-clean-branch
check-go-mod-tidy:
desc: Checks that go.mod and go.sum are up-to-date (requires a clean git working tree)
cmds:
@@ -101,10 +95,6 @@ tasks:
- cmd: grep -lr -E '^// Code generated - DO NOT EDIT\.$' tests/load/c | xargs -r rm
- cmd: go generate ./tests/load/c/...
generate-protobuf:
desc: Generates protobuf
cmd: ./scripts/protobuf_codegen.sh
ginkgo-build:
desc: Runs ginkgo against the current working directory
cmd: ./bin/ginkgo build {{.USER_WORKING_DIR}}
@@ -0,0 +1,107 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package chains
import (
"context"
"sync"
"testing"
"github.com/stretchr/testify/require"
"github.com/luxfi/ids"
"github.com/luxfi/log"
version "github.com/luxfi/version"
"github.com/luxfi/vm/chain"
)
// recordingConnector is a chain.ChainVM that records the version it is handed on
// Connected. Every other method comes from the embedded (nil) interface and is
// never invoked by the connect path.
type recordingConnector struct {
chain.ChainVM
mu sync.Mutex
called bool
gotVersion *version.Application
}
func (c *recordingConnector) Connected(_ context.Context, _ ids.NodeID, nodeVersion *chain.VersionInfo) error {
c.mu.Lock()
defer c.mu.Unlock()
c.called = true
c.gotVersion = nodeVersion
return nil
}
func (c *recordingConnector) Disconnected(context.Context, ids.NodeID) error { return nil }
// TestBlockHandlerConnectedWithVersionDeliversRealVersion is the regression
// guard for RED CRITICAL #1 (C-Chain nil-version panic).
//
// Before the fix blockHandler.Connected forwarded connector.Connected(ctx,
// nodeID, nil) — a hardcoded nil version. proposervm promotes Connected to the
// inner C-Chain VM (coreth), whose state-sync peer tracker compares peer
// versions; a nil version there dereferences nil (version.Application.Compare)
// and PANICS a state-syncing node (a fresh join, or a validator rejoining after
// falling behind — the launch's core invariant).
//
// The fix plumbs the REAL peer version through ConnectedWithVersion to the
// connector. This test drives the real blockHandler with a real version and
// asserts the connector receives that non-nil version — never nil.
func TestBlockHandlerConnectedWithVersionDeliversRealVersion(t *testing.T) {
require := require.New(t)
rc := &recordingConnector{}
bh := newBlockHandler(
nil, // BlockBuilder — unused by the connect path
rc, // connector under test
log.Noop(), // logger
nil, // engine
nil, // net
nil, // msgCreator
ids.Empty, // chainID
ids.Empty, // networkID
nil, // beacons
ids.NodeID{}, // selfNodeID
false, // expectsStakedBeacons
)
nodeID := ids.GenerateTestNodeID()
peerVersion := &version.Application{Name: "lux", Major: 1, Minor: 36, Patch: 27}
require.NoError(bh.ConnectedWithVersion(context.Background(), nodeID, peerVersion))
rc.mu.Lock()
defer rc.mu.Unlock()
require.True(rc.called, "connector.Connected must be invoked")
require.NotNil(rc.gotVersion, "connector must receive a NON-nil version (coreth dereferences it in state-sync)")
require.Equal("lux", rc.gotVersion.Name)
require.Equal(1, rc.gotVersion.Major)
require.Equal(36, rc.gotVersion.Minor)
require.Equal(27, rc.gotVersion.Patch)
}
// TestBlockHandlerConnectedDedupsButStillCarriesVersion confirms the version
// survives the once-only dedup: the first (versioned) dispatch reaches the
// connector; a duplicate is a no-op (not a nil-version overwrite).
func TestBlockHandlerConnectedDedupsButStillCarriesVersion(t *testing.T) {
require := require.New(t)
rc := &recordingConnector{}
bh := newBlockHandler(nil, rc, log.Noop(), nil, nil, nil, ids.Empty, ids.Empty, nil, ids.NodeID{}, false)
nodeID := ids.GenerateTestNodeID()
peerVersion := &version.Application{Name: "lux", Major: 1, Minor: 36, Patch: 27}
require.NoError(bh.ConnectedWithVersion(context.Background(), nodeID, peerVersion))
// A duplicate connect (e.g. dispatched again per tracked network) must be a
// no-op — never a second call that could clobber the stored version with nil.
require.NoError(bh.ConnectedWithVersion(context.Background(), nodeID, nil))
rc.mu.Lock()
defer rc.mu.Unlock()
require.NotNil(rc.gotVersion, "the deduped duplicate must not overwrite the real version with nil")
require.Equal(27, rc.gotVersion.Patch)
}
+49 -7
View File
@@ -57,14 +57,28 @@ const (
// tip, so a single (even >⅔-stake) validator cannot alone determine the frontier.
// Capped at the beacon-set size for tiny networks.
bootstrapMinAgreeingBeacons = 2
// bootstrapNamingWindow bounds the ancestry the ANCESTOR-TOLERANT frontier tally fetches
// per candidate anchor to resolve the bleeding-edge skew between honest beacons. On a live
// chain that skew is tiny (the canary was ONE block: 2 producers at N, 1 at N+1), so a
// single 256-block window covers it with enormous margin. A ⅔-common height more than this
// far below the highest tip is not a healthy bleeding-edge split — the tally names nothing
// (the loop then retries / fails safe), never a wrong block. Matches the consensus descent's
// per-round fetch size.
// bootstrapNamingWindow is the PER-FETCH ancestry chunk of the ANCESTOR-TOLERANT frontier
// tally. Ancestry returns FULL BLOCKS, so one fetch must stay inside a network message —
// this is a TRANSPORT bound, matching the consensus descent's per-round fetch size.
//
// It was previously ALSO doing two other jobs: the total ancestry budget, and a HEALTH
// HEURISTIC ("a ⅔-common height more than this far below the highest tip is not a healthy
// bleeding-edge split"). That heuristic is true for a LIVE chain and FALSE for a HALTED
// one, where the skew between a straggler and a node that ran on alone is arbitrarily large
// and perfectly healthy — no fork, just a stopped chain. Conflating the three WEDGED
// mainnet 96369: responders were split 1098726 (1 node) / 1098191 (2 nodes), 1098191 IS an
// ancestor of 1098726, and the ⅔-of-responder floor would have named it — but the gap was
// 535 blocks, the single 256-block fetch never reached down far enough for the high tip to
// vouch for it, and the tally named nothing on every retry, forever. Recovery from a halt
// is exactly when this path matters most, and it was disabled precisely then.
// The total budget is now maxNamingDepth; the health heuristic is gone.
bootstrapNamingWindow = 256
// bootstrapMaxNamingDepth is the TOTAL ancestry one anchor may be walked down, in
// bootstrapNamingWindow-sized chunks. Purely a resource bound (with maxNamingAnchors and
// bootstrapNamingTimeout): 32768 covers ~9h of 1s blocks of halt-skew, and the common
// healthy case never fetches at all — a single tip clearing the floor takes the exact fast
// path with zero fetches. Safety does NOT come from this number; see maxNamingDepth().
bootstrapMaxNamingDepth = 32768
// maxNamingAnchors bounds how many DISTINCT reported tips the ancestor-tolerant tally will
// fetch ancestry for in one round. Honest beacons cluster on a handful of adjacent tips, so
// this is never reached in practice; it caps the work a Byzantine swarm reporting many
@@ -201,6 +215,19 @@ func (b *blockHandler) fullyConnectedBeacons(weights map[ids.NodeID]uint64, conn
return external > 0 && len(connected) >= external
}
// hasExternalBeacons reports whether the staked beacon set contains any validator OTHER than this
// node. False for a self-only (single-validator) set — there is then no peer to sync from, so the
// node's own tip IS the frontier (FrontierTip returns FrontierNoBeacons). The set is read from
// P-chain STATE, not connectivity, so this is a TOPOLOGY fact (a genuine single-validator net),
// never an eclipse artifact (an eclipse drops peers from `connected`, never from `weights`).
func (b *blockHandler) hasExternalBeacons(weights map[ids.NodeID]uint64) bool {
external := len(weights)
if _, selfIsBeacon := weights[b.selfNodeID]; selfIsBeacon {
external--
}
return external > 0
}
// withSelfVote returns `replies` plus the node's OWN accepted frontier as a beacon reply — the
// SELF-VOTE. The node is itself a beacon (selfNodeID in `weights`, the trust anchor) and knows its
// own accepted tip (lastID) with certainty, so it vouches for it exactly as a connected peer's reply
@@ -311,6 +338,21 @@ func (b *blockHandler) FrontierTip(ctx context.Context) (ids.ID, chainbootstrap.
return ids.Empty, chainbootstrap.FrontierConnecting
}
// SELF-ONLY staked set: the configured validator set is exactly THIS node — a genuine
// single-validator network (a sybil-protected devnet, or the sole validator of an L1). There
// is no OTHER beacon to sync from, so the node's own accepted tip IS the network frontier →
// immediate start (FrontierNoBeacons). This is NOT an eclipse: the staked set is read from
// P-chain STATE, not connectivity, so a hidden peer would still appear in `weights`; only a
// genuine single-validator topology reaches here. Placed AFTER the P-ready gate so a boot-race
// partial set (transiently self-only mid-P-replay) WAITS above rather than false-completing here.
// This is the sybil-protected single-validator counterpart to the empty-set FrontierNoBeacons
// path: a multi-validator staked set (the ≥2 case) still runs the ⅔-by-stake quorum below.
if !b.hasExternalBeacons(weights) {
b.logger.Debug("bootstrap frontier: self-only staked set (single validator) — NoBeacons, nothing to sync to",
log.Stringer("chainID", b.chainID))
return ids.Empty, chainbootstrap.FrontierNoBeacons
}
// THE MASS-RECOVERY FIX. The acceptance decision is the BootstrapPolicy (a SEPARATE object
// with a SEPARATE threat model — bootstrap_trust.go), NOT the ⅔-of-CURRENT-total-stake
// consensus floor. The prior code required a ⅔-by-stake quorum of the WHOLE validator set to
+127
View File
@@ -25,6 +25,7 @@ import (
consensuschain "github.com/luxfi/consensus/engine/chain"
cblock "github.com/luxfi/consensus/engine/chain/block"
chainbootstrap "github.com/luxfi/consensus/engine/chain/bootstrap"
"github.com/luxfi/constants"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/math/set"
@@ -2032,3 +2033,129 @@ func TestRED_BehindNode_PeerConnectDelay_NeverFalseCompletesAtStale(t *testing.T
require.Greater(t, net.peerInfoCalls, net.connectAfterCalls,
"the loop must have WAITED through the connecting passes before naming the frontier")
}
// TestChainExpectsStakedBeacons_SybilDrivesNotSkipBootstrap pins the ROOT-CAUSE decision of the
// rejoin wedge (tasks #66/#74): whether a chain syncs its bootstrap frontier against the STAKED
// primary-network set is driven by SYBIL PROTECTION, NOT --skip-bootstrap. Production validators
// set --skip-bootstrap (to skip the initial bootstrap WAIT); the prior `!m.SkipBootstrap && ...`
// made that flag also EMPTY the beacon set, so a behind native chain (C/X/Q) reported
// FrontierNoBeacons, named its STALE local tip the network frontier, went live there, and never
// caught up across restarts until a manual chaindata wipe. skip-bootstrap is not even an input to
// the fixed decision — that is the point.
func TestChainExpectsStakedBeacons_SybilDrivesNotSkipBootstrap(t *testing.T) {
require.True(t, chainExpectsStakedBeacons(true, true, false),
"sybil-protected native non-platform chain (C/X/Q) MUST expect staked beacons → peer-sync a behind validator (regardless of skip-bootstrap)")
require.False(t, chainExpectsStakedBeacons(false, true, false),
"non-sybil (dev / single-node) native chain → NO staked beacons: the empty-beacon immediate-start path")
require.False(t, chainExpectsStakedBeacons(true, false, false),
"a non-native chain does not sync against the primary staked set here")
require.False(t, chainExpectsStakedBeacons(true, true, true),
"the platform chain anchors to its OWN CustomBeacons, never the staked-set frontier quorum")
}
// TestChainValidatesOnPrimaryNetwork_RealHashChainID is the regression the hardcoded-bool test
// above could NOT catch: it exercises the ACTUAL discriminator buildChain feeds into
// chainExpectsStakedBeacons. The durable rejoin fix (#66/#74) shipped INERT in production because
// the discriminator keyed off the blockchain ID via ids.IsNativeChain — false for every deployed
// C/X/Q (they carry HASH blockchain IDs; ids.IsNativeChain only matches the symbolic 111...C alias
// form, which no deployed chain uses). So a real behind C-Chain still got empty beacons under
// --skip-bootstrap and wedged. The fix keys off the validating Net (chainParams.ChainID) instead.
func TestChainValidatesOnPrimaryNetwork_RealHashChainID(t *testing.T) {
// Real deployed C-Chain blockchain IDs (devnet + mainnet) are HASHES, and ids.IsNativeChain
// is false for them — the exact trap that disabled the fix.
for _, s := range []string{
"21HieZngQW8unBnSTbdQ9PcPAz6hhPLGrPzZhTvjC8KgjE95Bg", // devnet C-Chain
"2wRdZGeca1qkxzNCq88NWDF5nJ5A9o623vRJKd3FsjRYvuVvvt", // mainnet C-Chain
} {
id, err := ids.FromString(s)
require.NoError(t, err)
require.False(t, ids.IsNativeChain(id),
"trap: ids.IsNativeChain is false for real hash C-Chain ID %s — do NOT discriminate on the blockchain ID", s)
}
// C/X/Q validate on the PRIMARY NETWORK: chainParams.ChainID == PrimaryNetworkID. That is the
// signal buildChain now passes, so the durable fix fires for the real (hash-ID) C-Chain.
require.True(t, chainValidatesOnPrimaryNetwork(constants.PrimaryNetworkID))
require.True(t, chainExpectsStakedBeacons(true, chainValidatesOnPrimaryNetwork(constants.PrimaryNetworkID), false),
"a sybil-protected primary-network non-platform chain (C/X/Q) MUST expect staked beacons regardless of its hash blockchain ID")
// An L2 validates on its OWN sovereign net, not the primary network → stays on the
// empty-beacon single-node path (behavior unchanged for L2s).
l2Net := ids.GenerateTestID()
require.False(t, chainValidatesOnPrimaryNetwork(l2Net))
require.False(t, chainExpectsStakedBeacons(true, chainValidatesOnPrimaryNetwork(l2Net), false))
}
// TestNodeBootstrap_SelfOnlyStakedSet_ReportsNoBeacons covers the single-VALIDATOR counterpart of
// the fix: a sybil-protected chain whose staked set is EXACTLY this node (a single-validator
// devnet, or the sole validator of an L1) has no OTHER beacon to sync from, so FrontierTip reports
// FrontierNoBeacons (immediate start at the node's own tip) rather than hanging in FrontierConnecting.
// This is what lets skip-bootstrap stop emptying native beacons WITHOUT bricking a single-validator
// net. It is NOT an eclipse: the staked set is read from P-chain STATE (hasExternalBeacons), so a
// hidden peer would still appear in `weights`.
func TestNodeBootstrap_SelfOnlyStakedSet_ReportsNoBeacons(t *testing.T) {
const N = 10
chain, byID := buildBSChain(N, -1)
vm := newBSVMAt(chain, N) // the sole validator IS at the frontier
self := ids.GenerateTestNodeID()
bh, chainID := newBSHandlerWeighted(t, vm, map[ids.NodeID]uint64{self: 100})
bh.selfNodeID = self // the staked set is EXACTLY this node — a genuine single-validator net
bh.net = &bsBeaconNet{bh: bh, chainID: chainID, byID: byID, tip: chain[N]}
bh.msgCreator = bsMsgBuilder{}
bh.bsActive.Store(true)
tip, status := bh.FrontierTip(context.Background())
bh.bsActive.Store(false)
require.Equal(t, chainbootstrap.FrontierNoBeacons, status,
"self-only staked set (single validator) → NoBeacons (nothing to sync to), never a FrontierConnecting hang")
require.Equal(t, ids.Empty, tip)
// Discriminator: add ONE other validator and the SAME node must now run the quorum (has a peer
// to sync from) — proving the self-only shortcut fires ONLY for a genuinely alone validator.
require.True(t, bh.hasExternalBeacons(map[ids.NodeID]uint64{self: 100, ids.GenerateTestNodeID(): 100}),
"a ≥2-validator staked set has an external beacon → runs the ⅔-by-stake quorum, not the self-only shortcut")
}
// TestNodeBootstrap_BehindValidator_StakedSet_CatchesUpNoWipe is THE REJOIN INVARIANT (tasks
// #66/#74), the code reproduction of the mainnet luxd-0 wedge: a staked validator that fell behind
// must sync from its peers on restart and reach the network frontier WITHOUT a chaindata wipe.
//
// The node reloads its STALE on-disk tip at height M (the "restart" — no wipe) and holds a
// sybil-protected native-chain staked beacon set (expectsStakedBeacons=true — exactly what
// buildChain now leaves in place under --skip-bootstrap, instead of emptying it). The 4 healthy
// producers name+serve the frontier N over the real GetAcceptedFrontier/GetAncestors transport, so
// the behind node fetch+executes M+1..N and ends ACCEPTED at N — never stuck at the stale M.
func TestNodeBootstrap_BehindValidator_StakedSet_CatchesUpNoWipe(t *testing.T) {
const N = 60 // network frontier (the healthy producers)
const M = 42 // our STALE local height after a restart — behind by N-M, NO wipe
chain, byID := buildBSChain(N, -1)
vm := newBSVMAt(chain, M) // reload the stale on-disk tip (the restart)
// A 5-validator staked set (equal stake): this node is one, the other 4 are the connected,
// serving producers at the frontier N. This mirrors lux-mainnet's 5-validator C-Chain.
weights := map[ids.NodeID]uint64{}
producers := make([]ids.NodeID, 4)
for i := range producers {
producers[i] = ids.GenerateTestNodeID()
weights[producers[i]] = 100
}
self := ids.GenerateTestNodeID()
weights[self] = 100
bh, chainID := newBSHandlerWeighted(t, vm, weights)
bh.selfNodeID = self
require.True(t, bh.expectsStakedBeacons,
"a native sybil chain expects staked beacons even under skip-bootstrap — the fix that keeps peer-sync alive")
bh.net = &bsBeaconNet{bh: bh, chainID: chainID, connected: producers, byID: byID, tip: chain[N], serveAncestors: true}
bh.msgCreator = bsMsgBuilder{}
ctx := context.Background()
require.NoError(t, runBS(t, bh),
"behind validator must converge to the frontier from its peers — no wipe, chain keeps finalizing")
last, _ := vm.LastAccepted(ctx)
require.Equal(t, chain[N].id, last,
"REJOIN: behind validator caught up from peers to frontier N=%d (was stuck at stale M=%d, no wipe)", N, M)
require.True(t, bh.Accepted(ctx, chain[N].id),
"node holds + ACCEPTED the frontier tip after the rejoin (genuine catch-up, not a stale false-complete)")
}
+52 -11
View File
@@ -243,10 +243,13 @@ type BootstrapPolicy struct {
// CheckpointVerifier authenticates the Checkpoint's authority signature (INVARIANT 4). nil ⇒ a
// configured Checkpoint is NOT trusted (fail closed) — a bare (id,height) is never enough.
CheckpointVerifier CheckpointVerifier
// NamingWindow bounds the ancestry fetched per anchor; MaxAnchors bounds how many distinct
// reported tips are resolved. Both default to the package constants when zero.
NamingWindow int
MaxAnchors int
// NamingWindow is the per-FETCH ancestry chunk (Ancestry returns FULL blocks, so one fetch
// must stay inside a network message); MaxNamingDepth is the TOTAL ancestry a single anchor
// may be walked down in NamingWindow-sized chunks; MaxAnchors bounds how many distinct
// reported tips are resolved. All default to the package constants when zero.
NamingWindow int
MaxNamingDepth int
MaxAnchors int
// NamingTimeout TOTAL-bounds the ancestor-tolerant resolution (all anchor fetches combined) so
// a partition that ANSWERS the frontier query but WITHHOLDS ancestry cannot make the decision
// hang — it returns what it found (or nothing → ErrNoBootstrapQuorum) and the caller's bounded
@@ -299,6 +302,17 @@ func (p *BootstrapPolicy) namingWindow() int {
return bootstrapNamingWindow
}
// maxNamingDepth is the TOTAL ancestry one anchor may be walked down, in namingWindow-sized
// chunks. It is a RESOURCE bound and nothing else. Safety comes from hash-verified ancestry
// (a forged chain cannot link), MinResponders distinct voters, the ⅔-of-RESPONDER-stake floor,
// and the full re-Verify every block gets on the descent — none of which depend on this number.
func (p *BootstrapPolicy) maxNamingDepth() int {
if p.MaxNamingDepth > 0 {
return p.MaxNamingDepth
}
return bootstrapMaxNamingDepth
}
func (p *BootstrapPolicy) maxAnchors() int {
if p.MaxAnchors > 0 {
return p.MaxAnchors
@@ -505,14 +519,41 @@ func (p *BootstrapPolicy) nameFrontier(ctx context.Context, stakeOnTip map[ids.I
break
}
fetches++
refs, err := p.Source.Ancestry(ctx, tip, p.namingWindow())
if err != nil {
continue
}
for _, ref := range refs {
if _, ok := index[ref.ID]; !ok {
index[ref.ID] = ref
// CHUNKED DESCENT. namingWindow is the per-FETCH size (Ancestry returns FULL blocks, so
// one fetch must fit a network message) — NOT the total ancestry worth walking. Keep
// following the parent chain in window-sized chunks up to maxNamingDepth, so a fleet
// that HALTED with a straggler far below the highest tip still resolves its ⅔-backed
// common ancestor. A single un-chunked window silently capped this at 256 and wedged
// mainnet at a 535-block gap. ctx already TOTAL-bounds every fetch (namingTimeout).
cur := tip
for depth := 0; depth < p.maxNamingDepth(); {
// The deadline must bind the WALK, not just each fetch: a Byzantine peer serving a
// long fabricated chain cheaply (or an in-process Source) would otherwise run the
// full depth budget before anyone checked the clock.
if ctx.Err() != nil {
break
}
refs, err := p.Source.Ancestry(ctx, cur, p.namingWindow())
if err != nil || len(refs) == 0 {
break
}
deepest, first := BlockRef{}, true
for _, ref := range refs {
if _, ok := index[ref.ID]; !ok {
index[ref.ID] = ref
}
if first || ref.Height < deepest.Height {
deepest, first = ref, false
}
}
depth += len(refs) // len(refs) >= 1 here, so depth strictly advances -> terminates
if deepest.Parent == ids.Empty {
break // reached genesis
}
if _, covered := index[deepest.Parent]; covered {
break // an earlier anchor's chain already covers everything below
}
cur = deepest.Parent
}
}
if len(index) == 0 {
+172
View File
@@ -916,3 +916,175 @@ func TestBootstrapTrust_EclipseOwnHeightNotNamedRoutesToCaughtUp(t *testing.T) {
require.Equal(t, refs[N].ID, f.ID, "boundary: N is named iff its height is STRICTLY ABOVE MinFrontierHeight")
require.Equal(t, uint64(N), f.Height)
}
// ----- H: HALT-SKEW RECOVERY (mainnet 96369 wedge) ---------------------------
// TestBootstrapTrust_H_HaltSkewDeeperThanOneWindow reproduces the live mainnet 96369 wedge and
// proves the chunked descent fixes it.
//
// SHAPE (measured per-node, in-pod, 2026-07-28): the fleet HALTED below its α=4-of-5 threshold.
// One node ran on alone to 1098726 while two stayed at 1098191; 1098191 IS an ancestor of
// 1098726 (no fork — luxd-3/luxd-4's `latest` IS 1098191, they hold nothing competing), so the
// ⅔-of-RESPONDER floor is satisfied at 1098191 by all three responders and it MUST be named.
//
// It was not. nameFrontier fetched one bootstrapNamingWindow (256) of ancestry per anchor, and
// the gap is 535 — so the high tip's ancestry never reached down far enough to vouch for the
// common block. 1098191 held only its 2 direct responders (2 of 3 = below the ⅔ floor of 2,
// which the strict `>` rejects), the tally named NOTHING, and every retry did the same. The
// node sat at height 0 forever and mainnet could not regain quorum.
//
// The gap here (535) is deliberately > one window (256) and < maxNamingDepth. With the
// single-fetch window this FAILS (frontier is nil → ErrNoBootstrapQuorum); with the chunked
// descent the walk continues past the first chunk and names the common ancestor.
func TestBootstrapTrust_H_HaltSkewDeeperThanOneWindow(t *testing.T) {
const w uint64 = 100
const gap = 535 // luxd-1 1098726 - luxd-3/luxd-4 1098191, the measured mainnet skew
// common is the last block the whole fleet accepted; `ahead` extends it by `gap`.
refs, byID := refChain(1000)
common := refs[1000]
prev := common
for i := 0; i < gap; i++ {
c := childRef(prev)
byID[c.ID] = c
prev = c
}
ahead := prev
require.Equal(t, common.Height+gap, ahead.Height)
require.Greater(t, gap, bootstrapNamingWindow, "gap MUST exceed one fetch window or this proves nothing")
require.Less(t, gap, bootstrapMaxNamingDepth, "gap must stay inside the total depth budget")
beacons := nodeIDs(3) // the three responders with a live C-Chain
policy := &BootstrapPolicy{
TrustedBeacons: equalBeacons(beacons, w),
MinResponses: 3,
Source: &stubAncestry{byID: byID},
}
replies := []BeaconReply{
reply(beacons[0], ahead.ID, w), // luxd-1, ran on alone
reply(beacons[1], common.ID, w), // luxd-3
reply(beacons[2], common.ID, w), // luxd-4
}
f, err := policy.AcceptsFrontier(context.Background(), replies)
require.NoError(t, err, "a fleet split across ONE chain must name a frontier, not wedge")
require.NotNil(t, f)
require.Equal(t, common.ID, f.ID,
"must name the ⅔-backed COMMON ancestor: the high tip vouches for it via ancestry")
require.Equal(t, common.Height, f.Height)
}
// TestBootstrapTrust_H_HaltSkewBeyondDepthStillFailsSafe pins the resource bound's edge: a skew
// DEEPER than maxNamingDepth still names nothing rather than guessing. Depth is a work bound, so
// exhausting it must degrade to the SAME fail-safe as before, never to a wrong block.
func TestBootstrapTrust_H_HaltSkewBeyondDepthStillFailsSafe(t *testing.T) {
const w uint64 = 100
refs, byID := refChain(10)
common := refs[10]
prev := common
for i := 0; i < 64; i++ {
c := childRef(prev)
byID[c.ID] = c
prev = c
}
ahead := prev
beacons := nodeIDs(3)
policy := &BootstrapPolicy{
TrustedBeacons: equalBeacons(beacons, w),
MinResponses: 3,
NamingWindow: 4, // tiny chunk...
MaxNamingDepth: 8, // ...and a depth budget far shallower than the 64-block skew
Source: &stubAncestry{byID: byID},
}
_, err := policy.AcceptsFrontier(context.Background(), []BeaconReply{
reply(beacons[0], ahead.ID, w),
reply(beacons[1], common.ID, w),
reply(beacons[2], common.ID, w),
})
require.Error(t, err, "a skew beyond the depth budget must fail SAFE, never name a guess")
}
// TestBootstrapTrust_H_AcceptanceMatrix pins the owner-specified recovery matrix at the policy
// layer. Each case is the SAME five-validator mainnet shape with a different responder pattern.
//
// The rule being pinned is "highest verifiably-vouched descendant wins", NOT "numerically highest
// tip advertised". A tip whose ancestry does not link into the fleet's chain earns NO credit no
// matter how high it claims to be, so a Byzantine node cannot pull the fleet onto a fabricated
// chain by advertising a tall one.
func TestBootstrapTrust_H_AcceptanceMatrix(t *testing.T) {
const w uint64 = 100
mk := func() (BlockRef, BlockRef, map[ids.ID]BlockRef) {
refs, byID := refChain(600)
common := refs[600]
prev := common
for i := 0; i < 535; i++ { // the measured mainnet skew, > one 256 window
c := childRef(prev)
byID[c.ID] = c
prev = c
}
return common, prev, byID
}
t.Run("1_high_2_low_2_unavailable__names_common_ancestor", func(t *testing.T) {
common, ahead, byID := mk()
b := nodeIDs(3)
p := &BootstrapPolicy{TrustedBeacons: equalBeacons(b, w), MinResponses: 3, Source: &stubAncestry{byID: byID}}
f, err := p.AcceptsFrontier(context.Background(), []BeaconReply{
reply(b[0], ahead.ID, w), reply(b[1], common.ID, w), reply(b[2], common.ID, w)})
require.NoError(t, err)
require.Equal(t, common.ID, f.ID)
})
t.Run("3_high_2_unavailable__names_high_tip", func(t *testing.T) {
_, ahead, byID := mk()
b := nodeIDs(3)
p := &BootstrapPolicy{TrustedBeacons: equalBeacons(b, w), MinResponses: 3, Source: &stubAncestry{byID: byID}}
f, err := p.AcceptsFrontier(context.Background(), []BeaconReply{
reply(b[0], ahead.ID, w), reply(b[1], ahead.ID, w), reply(b[2], ahead.ID, w)})
require.NoError(t, err)
require.Equal(t, ahead.ID, f.ID, "unanimous high tip must be named, not an ancestor")
})
t.Run("fabricated_tall_tip_2_low__rejects_fake_names_common", func(t *testing.T) {
common, _, byID := mk()
// A Byzantine node advertises a tip on a chain of its own that never links into the
// fleet's history. Its ancestry earns credit only for ITS OWN blocks, never for the
// fleet's — so it cannot outvote the two honest low responders.
fakeRefs, fakeByID := refChain(9_000_000)
fake := fakeRefs[9_000_000]
for id, r := range fakeByID {
byID[id] = r
}
b := nodeIDs(3)
p := &BootstrapPolicy{TrustedBeacons: equalBeacons(b, w), MinResponses: 3, Source: &stubAncestry{byID: byID}}
f, err := p.AcceptsFrontier(context.Background(), []BeaconReply{
reply(b[0], fake.ID, w), reply(b[1], common.ID, w), reply(b[2], common.ID, w)})
// The fake tip earns credit ONLY on its own disjoint chain (100), far below the ⅔ floor
// of 200, so it can never be named however tall it claims to be. `common` earns exactly
// 200 from its two honest backers — and the floor is STRICT (`> floor`), so 200 is not
// enough either: the Byzantine node withheld the third vouch by sitting on a chain that
// does not link. Correct outcome is a SAFE HALT, not "fall back to the low tip".
// This is the honest BFT trade: 1 of 3 responders can cost LIVENESS, never SAFETY.
require.Error(t, err, "must halt safely; must NOT follow a taller unvouched chain")
require.Nil(t, f)
})
t.Run("two_conflicting_branches_same_height__halts_safely", func(t *testing.T) {
refs, byID := refChain(600)
common := refs[600]
// Two disjoint branches of equal height off the common block, each backed by one node,
// and NO responder majority on either. Nothing may be named by height tiebreak.
l, r := childRef(common), childRef(common)
byID[l.ID], byID[r.ID] = l, r
b := nodeIDs(3)
p := &BootstrapPolicy{
TrustedBeacons: equalBeacons(b, w), MinResponses: 3,
MinFrontierHeight: common.Height, // node already holds `common`; only a tip AHEAD may be named
Source: &stubAncestry{byID: byID},
}
_, err := p.AcceptsFrontier(context.Background(), []BeaconReply{
reply(b[0], l.ID, w), reply(b[1], r.ID, w), reply(b[2], common.ID, w)})
require.Error(t, err, "conflicting equal-height branches must halt safely, never pick by height")
})
}
+11 -4
View File
@@ -72,15 +72,22 @@ func (s *Nets) IsChainBootstrapped(chainID ids.ID) bool {
// Bootstrapping returns the chainIDs of any chains that are still
// bootstrapping.
//
// s.chains is keyed by NET id, not chain id. Reporting the key named the net
// instead of the chain: every primary-network chain that failed to converge
// surfaced in /v1/health as the single ID
// "11111111111111111111111111111111LpoYY" — constants.PrimaryNetworkID, i.e.
// ids.Empty — a "chain" the chain manager has never heard of, so the operator
// chasing it got "there is no chain with alias/ID". Worse, N stuck chains
// collapsed into one indistinguishable entry. Ask each net which of ITS chains
// are still bootstrapping; the net owns that set.
func (s *Nets) Bootstrapping() []ids.ID {
s.lock.RLock()
defer s.lock.RUnlock()
chainsBootstrapping := make([]ids.ID, 0, len(s.chains))
for chainID, chain := range s.chains {
if !chain.IsBootstrapped() {
chainsBootstrapping = append(chainsBootstrapping, chainID)
}
for _, chain := range s.chains {
chainsBootstrapping = append(chainsBootstrapping, chain.Bootstrapping()...)
}
return chainsBootstrapping
+44 -2
View File
@@ -158,12 +158,54 @@ func TestNetsBootstrapping(t *testing.T) {
chain, ok := chains.GetOrCreate(netID)
require.True(ok)
// Start bootstrapping
// Start bootstrapping. What comes back is the CHAIN that is syncing, never
// the net that holds it — this assertion used to demand netID, which is
// how the phantom "11111111111111111111111111111111LpoYY" survived review.
chain.AddChain(chainID)
bootstrapping := chains.Bootstrapping()
require.Contains(bootstrapping, netID)
require.Equal([]ids.ID{chainID}, bootstrapping)
require.NotContains(bootstrapping, netID)
// Finish bootstrapping
chain.Bootstrapped(chainID)
require.Empty(chains.Bootstrapping())
}
// The "bootstrapped" health check publishes Nets.Bootstrapping() verbatim as
// its message. s.chains is keyed by NET id, so returning the key reported
// constants.PrimaryNetworkID (ids.Empty, cb58
// "11111111111111111111111111111111LpoYY") as though it were an unbootstrapped
// CHAIN. Operators saw a chain ID the chain manager denies exists, and every
// stuck chain on the net collapsed into that one phantom entry.
func TestNetsBootstrappingReportsChainsNotNets(t *testing.T) {
require := require.New(t)
chains, err := NewNets(ids.EmptyNodeID, map[ids.ID]nets.Config{
constants.PrimaryNetworkID: {},
})
require.NoError(err)
primary, _ := chains.GetOrCreate(constants.PrimaryNetworkID)
cChainID := ids.GenerateTestID()
dChainID := ids.GenerateTestID()
primary.AddChain(cChainID)
primary.AddChain(dChainID)
bootstrapping := chains.Bootstrapping()
// The phantom: never the net's own ID.
require.NotContains(bootstrapping, constants.PrimaryNetworkID)
require.NotContains(
bootstrapping,
ids.Empty,
"health check reported the primary NET id as an unbootstrapped chain",
)
// Both stuck chains must be individually nameable.
require.ElementsMatch([]ids.ID{cChainID, dChainID}, bootstrapping)
primary.Bootstrapped(cChainID)
require.Equal([]ids.ID{dChainID}, chains.Bootstrapping())
primary.Bootstrapped(dChainID)
require.Empty(chains.Bootstrapping())
}
+183 -34
View File
@@ -58,6 +58,7 @@ import (
"github.com/luxfi/constants"
"github.com/luxfi/container/buffer"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/crypto/mldsa"
"github.com/luxfi/filesystem/perms"
"github.com/luxfi/log"
"github.com/luxfi/math/set"
@@ -68,6 +69,7 @@ import (
"github.com/luxfi/node/upgrade"
"github.com/luxfi/node/vms"
validators "github.com/luxfi/validators"
version "github.com/luxfi/version"
"github.com/luxfi/vm/fx"
// "github.com/luxfi/node/vms/metervm" // Temporarily disabled - needs consensus package updates
@@ -101,6 +103,16 @@ const (
defaultChannelSize = 1
initialQueueSize = 3
// vmStartupTimeout bounds each VM startup/lifecycle operation (Initialize,
// Linearize, SetState, CreateHandlers, router AddChain, state-sync). It must
// be generous enough to cover a cold coreth "Regenerate historical state"
// pass after an unclean shutdown (observed 67-134s on mainnet C-Chain),
// which the previous hardcoded 30s budget blew — the context was cancelled
// mid-init, so the VM was marked failed and its C-Chain route never
// registered (the recurring post-restart 404 that needed a second restart).
// Bounded so a genuinely hung VM still surfaces rather than hanging forever.
vmStartupTimeout = 10 * time.Minute
luxNamespace = constants.PlatformName + utilmetric.NamespaceSeparator + "lux"
handlerNamespace = constants.PlatformName + utilmetric.NamespaceSeparator + "handler"
meterchainvmNamespace = constants.PlatformName + utilmetric.NamespaceSeparator + "meterchainvm"
@@ -353,8 +365,19 @@ type ManagerConfig struct {
SybilProtectionEnabled bool
StakingTLSSigner crypto.Signer
StakingTLSCert *staking.Certificate
StakingBLSKey bls.Signer
TracingEnabled bool
// Strict-PQ proposer identity. When StakingMLDSASigner is non-nil, chains wrap
// their proposervm with ML-DSA-65 block signing so the signed block's
// Proposer() matches the ML-DSA-keyed validator set. Nil ⇒ classical TLS-leaf.
StakingMLDSASigner *mldsa.PrivateKey
StakingMLDSAPub []byte
// ProposerWindowDuration overrides the proposervm proposer-slot spacing (0 ⇒
// the 5s mainnet default). Small local/dev nets set it low for fast cadence.
ProposerWindowDuration time.Duration
// ProposerMinBlockDelay overrides the proposervm minimum block delay (0 ⇒ the
// 1s default). High-throughput / DEX nets set it low (e.g. 1ms).
ProposerMinBlockDelay time.Duration
StakingBLSKey bls.Signer
TracingEnabled bool
// Must not be used unless [TracingEnabled] is true as this may be nil.
Tracer trace.Tracer
Log log.Logger
@@ -842,7 +865,7 @@ func (m *manager) createChain(chainParams ChainParameters) {
m.Log.Info("VM implements CreateHandlers, calling it",
log.Stringer("chainID", chainParams.ID),
)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), vmStartupTimeout)
defer cancel()
handlers, err := vm.CreateHandlers(ctx)
if err != nil {
@@ -909,7 +932,7 @@ func (m *manager) createChain(chainParams ChainParameters) {
// Register chain with the router for message routing
if m.ManagerConfig.Router != nil {
routeCtx, routeCancel := context.WithTimeout(context.Background(), 30*time.Second)
routeCtx, routeCancel := context.WithTimeout(context.Background(), vmStartupTimeout)
defer routeCancel()
m.ManagerConfig.Router.AddChain(routeCtx, chainParams.ID, chain.Handler)
}
@@ -951,7 +974,7 @@ func (m *manager) createChain(chainParams ChainParameters) {
// Tell the chain to start processing messages.
// If the X, P, or C Chain panics, do not attempt to recover
if chain.Engine != nil {
startCtx, startCancel := context.WithTimeout(context.Background(), 30*time.Second)
startCtx, startCancel := context.WithTimeout(context.Background(), vmStartupTimeout)
defer startCancel()
chain.Engine.Start(startCtx, !m.CriticalChains.Contains(chainParams.ID))
@@ -1109,18 +1132,27 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
}
// expectsStakedBeacons gates the EMPTY-beacon-set behavior of the bootstrap frontier
// quorum (see blockHandler.expectsStakedBeacons). True ONLY for a native NON-platform
// chain (C/X/Q/...) on a sybil-protected network — those sync against the STAKED
// quorum (see blockHandler.expectsStakedBeacons). True for a native NON-platform chain
// (C/X/Q/...) on a SYBIL-PROTECTED (real staked) network — those sync against the STAKED
// primary-network validator set (m.Validators, populated by the already-bootstrapped
// P-chain), so an empty set means "not yet loaded / misconfig → wait then fail safe",
// NOT "single-node → done". The P-chain (CustomBeacons may be empty under endpoint-only
// --bootstrap-nodes) and skip-bootstrap keep the "empty ⇒ nothing to sync to" behavior.
// Computed BEFORE the skip-bootstrap override so it is false in single-node mode.
expectsStakedBeacons := !m.SkipBootstrap && ids.IsNativeChain(chainParams.ID) && !isPlatformChain
// NOT "single-node → done". Driven by sybil-protection, NOT --skip-bootstrap: a production
// validator sets skip-bootstrap and that must NOT make it masquerade as single-node and
// wedge behind at its stale local tip (tasks #66/#74). See chainExpectsStakedBeacons.
// Discriminate on the validating Net (chainParams.ChainID), NOT the blockchain ID:
// deployed C/X/Q carry HASH blockchain IDs, and ids.IsNativeChain only matches the
// symbolic 111...C alias form (no deployed chain has it). Keying off the blockchain ID
// (the prior ids.IsNativeChain(chainParams.ID)) was therefore ALWAYS false for the real
// C-Chain, leaving this fix inert — the exact wedge #66/#74 set out to kill fired anyway.
isPrimaryNetworkChain := chainValidatesOnPrimaryNetwork(chainParams.ChainID)
expectsStakedBeacons := chainExpectsStakedBeacons(m.SybilProtectionEnabled, isPrimaryNetworkChain, isPlatformChain)
// In skip-bootstrap mode, use empty beacons for all chains
// This enables single-node development mode
if m.SkipBootstrap {
// skip-bootstrap forces empty beacons (single-node immediate-start) for every chain EXCEPT
// one that syncs against the staked set — those MUST always catch a behind validator up from
// its peers, so emptying their beacons (the prior UNCONDITIONAL override) is precisely the
// rejoin wedge this fixes. A genuine single-node / dev net runs sybil-protection OFF, so its
// native chains still fall here and get the empty-beacon immediate-start path.
if m.SkipBootstrap && !expectsStakedBeacons {
beacons = &emptyValidatorManager{}
m.Log.Info("skip-bootstrap enabled - using empty beacons for single-node mode")
}
@@ -1261,24 +1293,31 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
if regErr != nil {
return nil, fmt.Errorf("failed to register proposervm metrics for chain %s: %w", chainParams.ID, regErr)
}
minBlkDelay := proposervm.DefaultMinBlockDelay
if m.ProposerMinBlockDelay > 0 {
minBlkDelay = m.ProposerMinBlockDelay
}
engineVM = proposervm.New(vmTyped, proposervm.Config{
Upgrades: m.Upgrades,
NetworkID: networkID, // CRITICAL-3: windower uses the SAME set ID as the cert side
MinBlkDelay: proposervm.DefaultMinBlockDelay,
NumHistoricalBlocks: proposervm.DefaultNumHistoricalBlocks,
StakingLeafSigner: m.StakingTLSSigner,
StakingCertLeaf: m.StakingTLSCert,
Registerer: proposervmReg,
Upgrades: m.Upgrades,
NetworkID: networkID, // CRITICAL-3: windower uses the SAME set ID as the cert side
MinBlkDelay: minBlkDelay,
NumHistoricalBlocks: proposervm.DefaultNumHistoricalBlocks,
StakingLeafSigner: m.StakingTLSSigner,
StakingCertLeaf: m.StakingTLSCert,
StakingMLDSASigner: m.StakingMLDSASigner,
StakingMLDSAPub: m.StakingMLDSAPub,
ProposerWindowDuration: m.ProposerWindowDuration,
Registerer: proposervmReg,
})
m.Log.Info("wrapping chain VM in proposervm for single-proposer-per-height block production",
log.Stringer("chainID", chainParams.ID),
log.Int("K", consensusParams.K),
log.Stringer("windowerNetworkID", networkID),
log.Duration("minBlockDelay", proposervm.DefaultMinBlockDelay))
log.Duration("minBlockDelay", minBlkDelay))
}
m.Log.Info("initializing VM", log.Stringer("chainID", chainParams.ID))
initCtx, initCancel := context.WithTimeout(context.Background(), 30*time.Second)
initCtx, initCancel := context.WithTimeout(context.Background(), vmStartupTimeout)
defer initCancel()
// Initialize THROUGH engineVM. proposervm.Initialize builds its windower
// from chainRuntime.ValidatorState, then initializes the inner VM with the
@@ -1346,7 +1385,7 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
}); ok {
m.Log.Info("linearizing DAG-native VM into linear block mode",
log.Stringer("chainID", chainParams.ID))
linCtx, linCancel := context.WithTimeout(context.Background(), 30*time.Second)
linCtx, linCancel := context.WithTimeout(context.Background(), vmStartupTimeout)
if err := linearVM.Linearize(linCtx, ids.Empty, toEngine); err != nil {
linCancel()
m.Log.Error("failed to linearize VM",
@@ -1379,7 +1418,7 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
}); ok {
m.Log.Info("transitioning VM to bootstrapping (initial sync gates normal operation)",
log.Stringer("chainID", chainParams.ID))
stateCtx, stateCancel := context.WithTimeout(context.Background(), 30*time.Second)
stateCtx, stateCancel := context.WithTimeout(context.Background(), vmStartupTimeout)
if err := stateVM.SetState(stateCtx, uint32(vm.Bootstrapping)); err != nil {
stateCancel()
m.Log.Error("failed to transition VM to bootstrapping",
@@ -1598,7 +1637,7 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
m.Log.Info("consensus engine started with Lux consensus (Photon → Wave → Focus)",
log.Stringer("chainID", chainParams.ID))
if blockBuilder != nil {
syncCtx, syncCancel := context.WithTimeout(context.Background(), 30*time.Second)
syncCtx, syncCancel := context.WithTimeout(context.Background(), vmStartupTimeout)
defer syncCancel()
lastAcceptedID, height, err := consensuschain.SyncStateFromVM(syncCtx, blockBuilder, consensusEngine.Transitive)
if err != nil {
@@ -1666,7 +1705,10 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
// through (blockBuilder == the P-chain-height wrapper on K>1, the inner VM
// on K==1), so the container bytes it parses match the bytes the engine
// framed — one codec, no raw-vs-wrapped split.
bh := newBlockHandler(blockBuilder, m.Log, consensusEngine, m.Net, m.MsgCreator, chainParams.ID, networkID, beacons, m.NodeID, expectsStakedBeacons)
// engineVM is the connectable VM (the proposervm on a wrapped chain, the
// inner VM otherwise); it carries Connected/Disconnected, which the router
// dispatches so the P-chain uptime tracker observes validator connectivity.
bh := newBlockHandler(blockBuilder, engineVM, m.Log, consensusEngine, m.Net, m.MsgCreator, chainParams.ID, networkID, beacons, m.NodeID, expectsStakedBeacons)
// Gate this native chain's bootstrap frontier-TRUST on the P-chain having finished its
// initial sync, so the staked beacon set (and thus the stake-majority floor denominator)
// is the TRUE full validator set, not a partial mid-replay set. Wired ONLY for native
@@ -1867,7 +1909,7 @@ func (m *manager) createDAG(
}
// Create a context for VM initialization with timeout
initCtx, cancelInit := context.WithTimeout(context.Background(), 30*time.Second)
initCtx, cancelInit := context.WithTimeout(context.Background(), vmStartupTimeout)
defer cancelInit() // Ensure cleanup on function exit
// Initialize VM if it supports Initialize
@@ -2035,7 +2077,7 @@ func (m *manager) createDAG(
// Register HTTP handlers for DAG VMs (exchangevm, qvm, etc.)
adapter := &dagVMAdapter{underlying: vmImpl}
dagHandlerCtx, dagHandlerCancel := context.WithTimeout(context.Background(), 30*time.Second)
dagHandlerCtx, dagHandlerCancel := context.WithTimeout(context.Background(), vmStartupTimeout)
defer dagHandlerCancel()
handlers, err := adapter.CreateHandlers(dagHandlerCtx)
if err != nil {
@@ -2654,6 +2696,43 @@ func (m *manager) getOrMakeVMGatherer(vmID ids.ID) (metrics.MultiGatherer, error
return vmGatherer, nil
}
// chainExpectsStakedBeacons reports whether a chain must sync its bootstrap frontier against the
// STAKED primary-network validator set — the ⅔-by-stake quorum that names the network frontier
// (blockHandler.expectsStakedBeacons, the C1 forged-chain gate). When true, --skip-bootstrap does
// NOT empty the chain's beacon set (buildChain below), so a behind validator always catches up
// from its peers.
//
// It is driven by SYBIL PROTECTION — the true "this is a real staked network" signal — and NOT by
// --skip-bootstrap. Production validators set --skip-bootstrap to skip the initial bootstrap WAIT,
// but that flag must NEVER disable peer-sync on a staked network. Keying this decision off
// --skip-bootstrap (the prior `!m.SkipBootstrap && ...`) is exactly what wedged a behind native
// chain (C/X/Q...): with skip-bootstrap set, the chain got expectsStakedBeacons=false + empty
// beacons, so FrontierTip reported FrontierNoBeacons ("nothing to sync to"), the node named its
// STALE local last-accepted the network frontier, went live there, and never caught up across
// restarts until a manual chaindata wipe (tasks #66/#74). A genuine single-node / dev network runs
// sybil-protection OFF, so it still takes the empty-beacon immediate-start path. The platform chain
// anchors to its own CustomBeacons (not the staked set), so it is excluded here as before; a
// single-VALIDATOR staked net (self-only set) is handled downstream by FrontierTip's
// hasExternalBeacons rule, which reads the LOADED set.
func chainExpectsStakedBeacons(sybilProtectionEnabled, isPrimaryNetworkChain, isPlatformChain bool) bool {
return sybilProtectionEnabled && isPrimaryNetworkChain && !isPlatformChain
}
// chainValidatesOnPrimaryNetwork reports whether a chain's validating Net IS the primary
// network — the correct discriminator for the "native" C/X/Q/... set that syncs its bootstrap
// frontier against the primary-network STAKED validator set. It keys off the validating Net
// (subnet) ID, NOT the blockchain ID: deployed C/X/Q carry HASH blockchain IDs, whereas
// ids.IsNativeChain only ever matches the symbolic 111...C alias form that NO deployed chain
// uses (only the P-chain, at PlatformChainID=111...P, has a symbolic blockchain ID — and it is
// excluded as the platform chain anyway). Keying the durable-rejoin discriminator off the
// blockchain ID (the prior ids.IsNativeChain(chainParams.ID)) therefore silently excluded every
// real C/X/Q and left the fix inert across restarts (#66/#74). The validating Net is
// PrimaryNetworkID for C/X/Q and each sovereign L1's own net ID for an L2, so this correctly
// keeps the empty-beacon single-node path for L2s while re-enabling peer-sync for C/X/Q.
func chainValidatesOnPrimaryNetwork(validatingNetID ids.ID) bool {
return validatingNetID == constants.PrimaryNetworkID
}
// emptyValidatorManager implements validators.Manager with no validators
type emptyValidatorManager struct{}
@@ -2769,6 +2848,15 @@ type blockHandler struct {
chainID ids.ID // Chain ID for message routing
networkID ids.ID // Network ID for validator routing
// connector receives peer connect/disconnect notifications routed from the
// node's chainRouter and forwards them to this chain's VM (e.g. the P-chain
// uptime tracker; other VMs use them for their own peer sets). connectedNodes
// dedups delivery so a peer the router dispatches once per tracked network is
// forwarded to the VM exactly once. A nil connector makes forwarding a no-op.
connector chain.ChainVM
connMu sync.Mutex
connectedNodes set.Set[ids.NodeID]
// Context sync support - when a block fails verification due to missing context,
// we request the prerequisite blocks from the peer to catch up
pendingContext map[ids.ID]contextRequest // Map from blockID to pending context request
@@ -2943,9 +3031,10 @@ type contextRequest struct {
timestamp time.Time
}
func newBlockHandler(vm consensuschain.BlockBuilder, logger log.Logger, engine *consensuschain.Runtime, net network.Network, msgCreator message.OutboundMsgBuilder, chainID ids.ID, networkID ids.ID, beacons validators.Manager, selfNodeID ids.NodeID, expectsStakedBeacons bool) *blockHandler {
func newBlockHandler(vm consensuschain.BlockBuilder, connector chain.ChainVM, logger log.Logger, engine *consensuschain.Runtime, net network.Network, msgCreator message.OutboundMsgBuilder, chainID ids.ID, networkID ids.ID, beacons validators.Manager, selfNodeID ids.NodeID, expectsStakedBeacons bool) *blockHandler {
return &blockHandler{
vm: vm,
connector: connector,
logger: logger,
engine: engine,
net: net,
@@ -2958,6 +3047,7 @@ func newBlockHandler(vm consensuschain.BlockBuilder, logger log.Logger, engine *
pendingContext: make(map[ids.ID]contextRequest),
maxContextBlocks: 256, // Default max context blocks to request/serve
pendingQbits: make(map[ids.ID][]QbitEvent),
connectedNodes: set.NewSet[ids.NodeID](16),
bsAncestorCh: make(map[uint32]chan [][]byte),
}
}
@@ -3976,9 +4066,69 @@ func (b *blockHandler) GetStateSummary(ctx context.Context, nodeID ids.NodeID, r
func (b *blockHandler) StateSummary(ctx context.Context, nodeID ids.NodeID, requestID uint32, summary []byte) error {
return nil
}
func (b *blockHandler) Connected(ctx context.Context, nodeID ids.NodeID) error { return nil }
func (b *blockHandler) Disconnected(ctx context.Context, nodeID ids.NodeID) error { return nil }
func (b *blockHandler) HealthCheck(ctx context.Context) (interface{}, error) { return nil, nil }
// Connected satisfies handler.Handler, whose interface carries no peer version.
// The real delivery path is ConnectedWithVersion (chainRouter uses it via the
// versionedConnector capability); this nil-version entry exists only for the
// interface contract and any non-router caller.
func (b *blockHandler) Connected(ctx context.Context, nodeID ids.NodeID) error {
return b.connect(ctx, nodeID, nil)
}
// ConnectedWithVersion forwards a peer connection WITH its real application
// version to this chain's VM. chainRouter invokes this (detecting the
// versionedConnector capability) so the version survives to the inner VM:
// proposervm promotes Connected to the C-Chain (coreth), whose state-sync peer
// tracker compares peer versions — a nil version there dereferences nil and
// panics a state-syncing node (a fresh join OR a validator rejoining after
// falling behind). This mirrors avalanchego, which delivers msg.NodeVersion to
// engine.Connected.
func (b *blockHandler) ConnectedWithVersion(ctx context.Context, nodeID ids.NodeID, nodeVersion *version.Application) error {
return b.connect(ctx, nodeID, nodeVersion)
}
// connect forwards a peer connection to this chain's VM exactly once, carrying
// nodeVersion (nil only on the interface path). The P-chain VM records it in its
// uptime tracker; the C-Chain/X-Chain VMs store the version for their peer sets.
// A nil connector makes this a no-op. On a forwarding error the dedup entry is
// rolled back so a subsequent dispatch of the same connection retries.
func (b *blockHandler) connect(ctx context.Context, nodeID ids.NodeID, nodeVersion *version.Application) error {
if b.connector == nil {
return nil
}
b.connMu.Lock()
if b.connectedNodes.Contains(nodeID) {
b.connMu.Unlock()
return nil
}
b.connectedNodes.Add(nodeID)
b.connMu.Unlock()
if err := b.connector.Connected(ctx, nodeID, nodeVersion); err != nil {
b.connMu.Lock()
b.connectedNodes.Remove(nodeID)
b.connMu.Unlock()
return err
}
return nil
}
// Disconnected forwards a peer disconnection to this chain's VM exactly once.
func (b *blockHandler) Disconnected(ctx context.Context, nodeID ids.NodeID) error {
if b.connector == nil {
return nil
}
b.connMu.Lock()
if !b.connectedNodes.Contains(nodeID) {
b.connMu.Unlock()
return nil
}
b.connectedNodes.Remove(nodeID)
b.connMu.Unlock()
return b.connector.Disconnected(ctx, nodeID)
}
func (b *blockHandler) HealthCheck(ctx context.Context) (interface{}, error) { return nil, nil }
func (b *blockHandler) Stop(ctx context.Context) {
if b.pollerCancel != nil {
b.pollerCancel()
@@ -4269,7 +4419,6 @@ func (g *networkGossiper) BroadcastVote(chainID ids.ID, networkID ids.ID, blockI
// it fed no longer exists in the consensus engine (174af3c31). Nova sampling decides; the ⅔
// Quasar attestation (a plain accept-vote, gossiped via BroadcastVote) trails it. Keep the braid dead.
// GossipCert broadcasts an assembled α-of-K finality cert to ALL validators so
// followers finalize blockID on a verifiable proof (HandleIncomingCert), not a
// fast-follow guess. Best effort: the gossiping node's own finality is already
-244
View File
@@ -1,244 +0,0 @@
# Robust RPC Handler Registration System
## Overview
This package provides a bulletproof RPC handler registration system for the Lux node, designed to handle the complexities of local development where nodes are frequently restarted. It replaces the fragile inline registration logic with a robust, maintainable solution.
## Key Features
### 🔄 Automatic Retry Logic
- Exponential backoff for transient failures
- Configurable retry count and wait times
- Context-aware cancellation support
### ✅ Built-in Health Checks
- Automatic validation after registration
- Batch health checking for all chains
- Detailed diagnostics for failures
### 🎯 Single Source of Truth
- Centralized route construction logic
- Consistent path formatting
- No duplicate code or magic strings
### 🛡️ Defensive Programming
- Nil checks on all inputs
- Handler validation before registration
- Graceful degradation on failures
### 📊 Developer-Friendly Debugging
- Clear, actionable error messages
- Comprehensive logging at appropriate levels
- Built-in diagnostic tools
## Architecture
```
┌─────────────────────┐
│ Chain Manager │
└──────────┬──────────┘
│ Creates Chain
┌─────────────────────┐
│ ChainHandlerRegistrar│
└──────────┬──────────┘
│ Extracts Handlers
┌─────────────────────┐
│ Handler Manager │
└──────────┬──────────┘
│ Registers with Retries
┌─────────────────────┐
│ API Server │
└─────────────────────┘
```
## Usage
### Basic Integration
Replace the handler registration code in `chains/manager.go` (lines 941-990) with:
```go
// Create robust registrar
registrar := rpc.NewChainHandlerRegistrar(
m.Server,
m.Log,
m.CChainID,
m.PChainID,
)
// Register handlers
if err := registrar.RegisterChainHandlers(ctx, chainParams.ID, chain.VM); err != nil {
m.Log.Error("Failed to register handlers", log.Err(err))
// Decide if this should be fatal or not
}
```
### Configuration
```go
// Development environment - fail fast
registrar.SetRetryConfig(2, 50*time.Millisecond)
// Production environment - more robust
registrar.SetRetryConfig(5, 200*time.Millisecond)
```
### Debugging
```go
// Get route information
info, exists := registrar.GetRouteInfo(chainID)
if exists {
fmt.Printf("Chain %s routes: %v\n", chainID, info.Endpoints)
}
// Run health checks
results := registrar.HealthCheckAll()
for chainID, healthy := range results {
fmt.Printf("Chain %s: %v\n", chainID, healthy)
}
// Validate specific endpoint
err := registrar.ValidateEndpoint(chainID, "/rpc")
```
### Using the Debug Tool
```go
// Quick diagnosis from CLI
rpc.QuickDiagnose("localhost:9650", chainID, "C")
// Programmatic diagnosis
tool := rpc.NewDebugTool("localhost:9650", logger)
report := tool.DiagnoseEndpoint(chainID, "C")
fmt.Println(report.String())
```
## Components
### HandlerManager (`handler_manager.go`)
Core registration logic with retry mechanism and health checks.
**Key Methods:**
- `RegisterChainHandlers()` - Main registration entry point
- `HealthCheckRoute()` - Validates handler responsiveness
- `GetRouteInfo()` - Retrieves registration details
### ChainHandlerRegistrar (`chain_integration.go`)
Bridge between chain manager and handler manager.
**Key Methods:**
- `RegisterChainHandlers()` - Extracts and registers handlers
- `ValidateEndpoint()` - Tests specific endpoints
- `GetAllRoutes()` - Returns all registered routes
### DebugTool (`debug_tool.go`)
Comprehensive endpoint diagnostics for developers.
**Key Methods:**
- `DiagnoseEndpoint()` - Full endpoint analysis
- `QuickDiagnose()` - CLI-friendly diagnosis
## Error Handling
The system uses clear, actionable errors:
```go
errNilHandler = errors.New("handler is nil")
errNilServer = errors.New("server is nil")
errEmptyEndpoint = errors.New("endpoint is empty")
errRegistrationFailed = errors.New("handler registration failed")
errHealthCheckFailed = errors.New("health check failed")
```
Each error includes context about what failed and why.
## Testing
Comprehensive test coverage including:
- Successful registration scenarios
- Validation failure cases
- Retry logic verification
- Health check validation
- Context cancellation
- Performance benchmarks
Run tests:
```bash
go test ./chains/rpc/... -v
```
## Common Issues and Solutions
### Issue: Handlers not accessible after registration
**Solution:** Check health status with `HealthCheckAll()` and review debug output.
### Issue: Registration fails with "already exists"
**Solution:** The retry logic handles this. If persistent, check for duplicate registration attempts.
### Issue: Slow registration during development
**Solution:** Reduce retry count and wait time using `SetRetryConfig()`.
### Issue: Can't find the correct endpoint URL
**Solution:** Use `DebugTool.DiagnoseEndpoint()` to test all URL patterns.
## Migration Guide
1. **Update imports:**
```go
import "github.com/luxfi/node/chains/rpc"
```
2. **Replace inline registration (lines 941-990 in manager.go):**
```go
// Old code: complex type checking and manual registration
// New code: single function call
registrar := rpc.NewChainHandlerRegistrar(...)
registrar.RegisterChainHandlers(...)
```
3. **Add health monitoring (optional):**
```go
go func() {
time.Sleep(5 * time.Second)
registrar.HealthCheckAll()
}()
```
4. **Add debugging endpoints (optional):**
```go
http.HandleFunc("/debug/handlers", func(w http.ResponseWriter, r *http.Request) {
routes := registrar.GetAllRoutes()
json.NewEncoder(w).Encode(routes)
})
```
## Performance
- Registration: ~1ms per handler (without retries)
- Health check: ~10ms per chain
- Memory overhead: ~1KB per registered chain
- No goroutine leaks or resource issues
## Future Improvements
Potential enhancements:
- Metrics integration for registration success/failure rates
- Automatic re-registration on failure
- WebSocket-specific health checks
- gRPC handler support
- Handler versioning for upgrades
## Philosophy
This implementation follows core Go principles:
- **Explicit over implicit** - Clear registration flow
- **Errors are values** - Proper error handling throughout
- **Simple over clever** - Straightforward retry logic
- **Composition over inheritance** - Small, focused components
- **Documentation is code** - Self-documenting with clear names
The system is designed to be bulletproof for development while remaining simple to understand and maintain.
-168
View File
@@ -1,168 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpc
import (
"context"
"fmt"
"net/http"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/node/server/http"
"github.com/luxfi/node/vms"
)
// ChainHandlerRegistrar provides a clean interface for chain manager to register handlers.
// This replaces the inline registration logic with a more robust, testable solution.
type ChainHandlerRegistrar struct {
manager *HandlerManager
server server.Server
log log.Logger
cChainID ids.ID // Special handling for C-Chain
pChainID ids.ID // Platform chain ID for validation
}
// NewChainHandlerRegistrar creates a registrar for chain handler registration.
// Encapsulates all the registration logic in one place.
func NewChainHandlerRegistrar(
server server.Server,
logger log.Logger,
cChainID ids.ID,
pChainID ids.ID,
) *ChainHandlerRegistrar {
return &ChainHandlerRegistrar{
manager: NewHandlerManager(server, logger),
server: server,
log: logger,
cChainID: cChainID,
pChainID: pChainID,
}
}
// RegisterChainHandlers is the main entry point from chain manager.
// Handles all the complexity of VM type checking and handler extraction.
func (r *ChainHandlerRegistrar) RegisterChainHandlers(
ctx context.Context,
chainID ids.ID,
vm interface{},
) error {
r.log.Info("Attempting to register chain handlers",
log.Stringer("chainID", chainID),
log.String("vmType", fmt.Sprintf("%T", vm)))
// Don't register handlers for Platform VM
if chainID == r.pChainID {
r.log.Debug("Skipping handler registration for Platform VM")
return nil
}
// Extract handlers from VM
handlers, err := r.extractHandlers(ctx, vm)
if err != nil {
return fmt.Errorf("failed to extract handlers: %w", err)
}
if len(handlers) == 0 {
r.log.Info("VM does not provide any handlers",
log.Stringer("chainID", chainID))
return nil
}
// Determine chain alias (special case for C-Chain)
alias := r.getChainAlias(chainID)
// Register with robust handler manager
return r.manager.RegisterChainHandlers(ctx, chainID, alias, handlers)
}
// extractHandlers attempts to get handlers from the VM using multiple strategies.
// Handles different VM wrapper types gracefully.
func (r *ChainHandlerRegistrar) extractHandlers(
ctx context.Context,
vm interface{},
) (map[string]http.Handler, error) {
// First try direct interface check
if provider, ok := vm.(vms.HandlerProvider); ok {
r.log.Debug("VM directly implements HandlerProvider")
return provider.CreateHandlers(ctx)
}
// Try using the delegate helper (handles wrapped VMs)
handlers, err := vms.DelegateHandlers(ctx, vm)
if err != nil {
return nil, fmt.Errorf("handler delegation failed: %w", err)
}
if len(handlers) > 0 {
r.log.Debug("Successfully extracted handlers via delegation",
log.Int("count", len(handlers)))
}
return handlers, nil
}
// getChainAlias returns the appropriate alias for a chain.
// C-Chain gets special treatment, others use their ID.
func (r *ChainHandlerRegistrar) getChainAlias(chainID ids.ID) string {
if chainID == r.cChainID {
return "C"
}
// Could extend this for X-Chain and P-Chain if needed
return ""
}
// GetRouteInfo returns information about a specific chain's registered routes.
// Useful for debugging and operational visibility.
func (r *ChainHandlerRegistrar) GetRouteInfo(chainID ids.ID) (*RouteInfo, bool) {
return r.manager.GetRouteInfo(chainID)
}
// GetAllRoutes returns all registered routes across all chains.
// Complete visibility for monitoring and debugging.
func (r *ChainHandlerRegistrar) GetAllRoutes() map[string]*RouteInfo {
return r.manager.GetAllRoutes()
}
// HealthCheckAll performs health checks on all registered routes.
// Returns a map of chainID -> healthy status.
func (r *ChainHandlerRegistrar) HealthCheckAll() map[string]bool {
return r.manager.HealthCheckAll()
}
// SetRetryConfig allows tuning of retry behavior for different environments.
// Production might want more retries, dev might want faster failures.
func (r *ChainHandlerRegistrar) SetRetryConfig(maxRetries int, initialWait time.Duration) {
r.manager.SetRetryConfig(maxRetries, initialWait)
}
// ValidateEndpoint performs a test request against a specific endpoint.
// Useful for debugging specific handler issues.
func (r *ChainHandlerRegistrar) ValidateEndpoint(
chainID ids.ID,
endpoint string,
) error {
info, exists := r.manager.GetRouteInfo(chainID)
if !exists {
return fmt.Errorf("no routes registered for chain %s", chainID)
}
// Build the full URL
fullURL := fmt.Sprintf("/v1/%s%s", info.Base, endpoint)
r.log.Info("Validating endpoint",
log.Stringer("chainID", chainID),
log.String("url", fullURL))
// Validates endpoint registration
for _, registered := range info.Endpoints {
if registered == endpoint {
return nil
}
}
return fmt.Errorf("endpoint %s not found in registered endpoints: %v",
endpoint, info.Endpoints)
}
-302
View File
@@ -1,302 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpc
import (
"bytes"
"github.com/go-json-experiment/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/log"
)
// DebugTool provides utilities for debugging RPC handler issues.
// Developer-friendly diagnostics with clear, actionable output.
type DebugTool struct {
baseURL string
client *http.Client
log log.Logger
}
// NewDebugTool creates a debug tool for RPC endpoint testing.
func NewDebugTool(baseURL string, logger log.Logger) *DebugTool {
if !strings.HasPrefix(baseURL, "http") {
baseURL = "http://" + baseURL
}
return &DebugTool{
baseURL: strings.TrimSuffix(baseURL, "/"),
client: &http.Client{
Timeout: 10 * time.Second,
},
log: logger,
}
}
// DiagnoseEndpoint performs comprehensive diagnostics on an RPC endpoint.
// Returns detailed information about what's working and what's not.
func (d *DebugTool) DiagnoseEndpoint(chainID ids.ID, alias string) *DiagnosticReport {
report := &DiagnosticReport{
ChainID: chainID,
Alias: alias,
Timestamp: time.Now(),
Tests: make([]TestResult, 0),
}
// Test different URL patterns
urlPatterns := d.getURLPatterns(chainID, alias)
for _, pattern := range urlPatterns {
result := d.testEndpoint(pattern)
report.Tests = append(report.Tests, result)
}
// Test common RPC methods
if bestURL := report.GetBestURL(); bestURL != "" {
report.RPCTests = d.testRPCMethods(bestURL)
}
return report
}
// getURLPatterns returns all possible URL patterns to test.
func (d *DebugTool) getURLPatterns(chainID ids.ID, alias string) []string {
patterns := []string{
fmt.Sprintf("%s/v1/bc/%s/rpc", d.baseURL, chainID.String()),
fmt.Sprintf("%s/v1/bc/%s/ws", d.baseURL, chainID.String()),
fmt.Sprintf("%s/v1/bc/%s", d.baseURL, chainID.String()),
}
if alias != "" && alias != chainID.String() {
patterns = append(patterns,
fmt.Sprintf("%s/v1/bc/%s/rpc", d.baseURL, alias),
fmt.Sprintf("%s/v1/bc/%s/ws", d.baseURL, alias),
fmt.Sprintf("%s/v1/bc/%s", d.baseURL, alias),
)
}
// Also test without /v1 prefix (some setups might differ)
patterns = append(patterns,
fmt.Sprintf("%s/bc/%s/rpc", d.baseURL, chainID.String()),
)
return patterns
}
// testEndpoint tests a single endpoint URL.
func (d *DebugTool) testEndpoint(url string) TestResult {
result := TestResult{
URL: url,
Timestamp: time.Now(),
}
// First try a simple GET
resp, err := d.client.Get(url)
if err != nil {
result.Error = fmt.Sprintf("GET failed: %v", err)
result.Success = false
return result
}
defer resp.Body.Close()
result.StatusCode = resp.StatusCode
// Read body for debugging
bodyBytes, _ := io.ReadAll(resp.Body)
result.Response = string(bodyBytes)
// Now try a POST with JSON-RPC
rpcReq := map[string]interface{}{
"jsonrpc": "2.0",
"method": "web3_clientVersion",
"params": []interface{}{},
"id": 1,
}
jsonBytes, _ := json.Marshal(rpcReq)
postResp, err := d.client.Post(url, "application/json", bytes.NewReader(jsonBytes))
if err != nil {
result.Error = fmt.Sprintf("POST failed: %v", err)
result.Success = false
return result
}
defer postResp.Body.Close()
result.StatusCode = postResp.StatusCode
// Check if we got a valid JSON-RPC response
var rpcResp map[string]interface{}
if err := json.UnmarshalRead(postResp.Body, &rpcResp); err == nil {
if _, hasResult := rpcResp["result"]; hasResult {
result.Success = true
result.Response = fmt.Sprintf("Valid JSON-RPC response: %v", rpcResp["result"])
} else if errObj, hasError := rpcResp["error"]; hasError {
result.Success = false
result.Response = fmt.Sprintf("JSON-RPC error: %v", errObj)
}
}
return result
}
// testRPCMethods tests common RPC methods against an endpoint.
func (d *DebugTool) testRPCMethods(url string) []RPCTest {
methods := []string{
"web3_clientVersion",
"eth_blockNumber",
"eth_chainId",
"net_version",
"eth_syncing",
}
tests := make([]RPCTest, 0, len(methods))
for _, method := range methods {
test := RPCTest{
Method: method,
URL: url,
}
req := map[string]interface{}{
"jsonrpc": "2.0",
"method": method,
"params": []interface{}{},
"id": 1,
}
jsonBytes, _ := json.Marshal(req)
resp, err := d.client.Post(url, "application/json", bytes.NewReader(jsonBytes))
if err != nil {
test.Error = err.Error()
test.Success = false
} else {
defer resp.Body.Close()
var result map[string]interface{}
if err := json.UnmarshalRead(resp.Body, &result); err == nil {
if res, ok := result["result"]; ok {
test.Success = true
test.Result = fmt.Sprintf("%v", res)
} else if errObj, ok := result["error"]; ok {
test.Error = fmt.Sprintf("%v", errObj)
}
}
}
tests = append(tests, test)
}
return tests
}
// DiagnosticReport contains comprehensive endpoint diagnostic information.
type DiagnosticReport struct {
ChainID ids.ID
Alias string
Timestamp time.Time
Tests []TestResult
RPCTests []RPCTest
}
// TestResult represents a single endpoint test result.
type TestResult struct {
URL string
Success bool
StatusCode int
Response string
Error string
Timestamp time.Time
}
// RPCTest represents a test of a specific RPC method.
type RPCTest struct {
Method string
URL string
Success bool
Result string
Error string
}
// GetBestURL returns the first working URL from the tests.
func (r *DiagnosticReport) GetBestURL() string {
for _, test := range r.Tests {
if test.Success {
return test.URL
}
}
return ""
}
// String returns a human-readable report.
func (r *DiagnosticReport) String() string {
var b strings.Builder
b.WriteString(fmt.Sprintf("=== RPC Endpoint Diagnostic Report ===\n"))
b.WriteString(fmt.Sprintf("Chain ID: %s\n", r.ChainID))
if r.Alias != "" {
b.WriteString(fmt.Sprintf("Alias: %s\n", r.Alias))
}
b.WriteString(fmt.Sprintf("Timestamp: %s\n\n", r.Timestamp.Format(time.RFC3339)))
b.WriteString("=== Endpoint Tests ===\n")
for _, test := range r.Tests {
status := "❌ FAILED"
if test.Success {
status = "✅ SUCCESS"
}
b.WriteString(fmt.Sprintf("\n%s %s\n", status, test.URL))
b.WriteString(fmt.Sprintf(" Status Code: %d\n", test.StatusCode))
if test.Error != "" {
b.WriteString(fmt.Sprintf(" Error: %s\n", test.Error))
}
if test.Response != "" && len(test.Response) < 200 {
b.WriteString(fmt.Sprintf(" Response: %s\n", test.Response))
}
}
if len(r.RPCTests) > 0 {
b.WriteString("\n=== RPC Method Tests ===\n")
for _, test := range r.RPCTests {
status := "❌"
if test.Success {
status = "✅"
}
b.WriteString(fmt.Sprintf("%s %s: ", status, test.Method))
if test.Success {
b.WriteString(test.Result)
} else {
b.WriteString(test.Error)
}
b.WriteString("\n")
}
}
b.WriteString("\n=== Recommendations ===\n")
if bestURL := r.GetBestURL(); bestURL != "" {
b.WriteString(fmt.Sprintf("✅ Use this endpoint: %s\n", bestURL))
} else {
b.WriteString("❌ No working endpoints found. Check:\n")
b.WriteString(" 1. Is the node running?\n")
b.WriteString(" 2. Is the chain bootstrapped?\n")
b.WriteString(" 3. Are handlers properly registered?\n")
b.WriteString(" 4. Check node logs for handler registration errors\n")
b.WriteString(" 5. Try restarting the node\n")
}
return b.String()
}
// QuickDiagnose performs a quick endpoint check and prints results.
// Convenience function for CLI tools.
func QuickDiagnose(nodeURL string, chainID ids.ID, alias string) {
logger := log.NewNoOpLogger()
tool := NewDebugTool(nodeURL, logger)
report := tool.DiagnoseEndpoint(chainID, alias)
fmt.Println(report.String())
}
-324
View File
@@ -1,324 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package rpc provides robust RPC handler registration with retries, health checks, and clear debugging.
// Follows Go principles: fail fast with clear errors, single responsibility, minimal dependencies.
package rpc
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"sync"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/node/server/http"
)
var (
// Errors follow Go convention: lowercase, descriptive, actionable
errNilHandler = errors.New("handler is nil")
errNilServer = errors.New("server is nil")
errEmptyEndpoint = errors.New("endpoint is empty")
errRegistrationFailed = errors.New("handler registration failed")
errHealthCheckFailed = errors.New("health check failed")
)
// HandlerManager manages RPC handler registration with robust error handling and health checks.
// Single responsibility: reliable handler registration with observability.
type HandlerManager struct {
server server.Server
log log.Logger
mu sync.RWMutex
routes map[string]*RouteInfo // chainID -> route info
retries int // max registration retries
retryWait time.Duration // initial retry wait time
}
// RouteInfo contains complete information about a registered route.
// Everything needed for debugging in one place.
type RouteInfo struct {
ChainID ids.ID
ChainAlias string
Base string // e.g., "bc/C" or "bc/<chainID>"
Endpoints []string // e.g., ["/rpc", "/ws"]
Handler http.Handler
Healthy bool
LastCheck time.Time
}
// NewHandlerManager creates a handler manager with sensible defaults.
// Simple factory, no magic.
func NewHandlerManager(server server.Server, logger log.Logger) *HandlerManager {
return &HandlerManager{
server: server,
log: logger,
routes: make(map[string]*RouteInfo),
retries: 3,
retryWait: 100 * time.Millisecond,
}
}
// RegisterChainHandlers registers all handlers for a chain with retry logic and health checks.
// This is the main entry point - handles everything needed for robust registration.
func (m *HandlerManager) RegisterChainHandlers(
ctx context.Context,
chainID ids.ID,
chainAlias string,
handlers map[string]http.Handler,
) error {
if m.server == nil {
return errNilServer
}
m.log.Info("Starting chain handler registration",
log.Stringer("chainID", chainID),
log.String("alias", chainAlias),
log.Int("handlerCount", len(handlers)))
// Validate handlers first - fail fast
if err := m.validateHandlers(handlers); err != nil {
return fmt.Errorf("handler validation failed: %w", err)
}
// Build route info
info := &RouteInfo{
ChainID: chainID,
ChainAlias: chainAlias,
Endpoints: make([]string, 0, len(handlers)),
}
// Determine base paths
bases := m.getBasePaths(chainID, chainAlias)
// Register each handler with retries
var registrationErrors []error
for endpoint, handler := range handlers {
info.Endpoints = append(info.Endpoints, endpoint)
for _, base := range bases {
if err := m.registerWithRetry(ctx, base, endpoint, handler); err != nil {
registrationErrors = append(registrationErrors,
fmt.Errorf("failed to register %s%s: %w", base, endpoint, err))
m.log.Error("Handler registration failed",
log.String("base", base),
log.String("endpoint", endpoint),
log.Err(err))
} else {
m.log.Info("Handler registered successfully",
log.String("route", fmt.Sprintf("/v1/%s%s", base, endpoint)),
log.Stringer("chainID", chainID))
}
}
}
// Store route info for monitoring
m.mu.Lock()
info.Base = bases[0] // Primary base
info.Handler = handlers["/rpc"] // Store primary handler for health checks
m.routes[chainID.String()] = info
m.mu.Unlock()
// Run health checks
if err := m.healthCheckRoute(info); err != nil {
m.log.Warn("Health check failed for newly registered chain",
log.Stringer("chainID", chainID),
log.Err(err))
}
// Return aggregate error if any registrations failed
if len(registrationErrors) > 0 {
return fmt.Errorf("%w: %v", errRegistrationFailed, registrationErrors)
}
m.log.Info("Chain handler registration completed",
log.Stringer("chainID", chainID),
log.String("routes", strings.Join(m.getFullRoutes(bases, info.Endpoints), ", ")))
return nil
}
// validateHandlers ensures all handlers are valid before attempting registration.
// Fail fast with clear errors - no silent failures.
func (m *HandlerManager) validateHandlers(handlers map[string]http.Handler) error {
if len(handlers) == 0 {
return errors.New("no handlers provided")
}
for endpoint, handler := range handlers {
if handler == nil {
return fmt.Errorf("%w for endpoint %s", errNilHandler, endpoint)
}
if endpoint == "" {
return errEmptyEndpoint
}
// Ensure endpoint starts with /
if !strings.HasPrefix(endpoint, "/") {
return fmt.Errorf("endpoint %s must start with /", endpoint)
}
}
return nil
}
// getBasePaths returns all base paths for a chain (with and without alias).
// Single source of truth for path construction.
func (m *HandlerManager) getBasePaths(chainID ids.ID, chainAlias string) []string {
bases := []string{}
// If we have an alias (like "C" for C-Chain), use it as primary
if chainAlias != "" && chainAlias != chainID.String() {
bases = append(bases, fmt.Sprintf("bc/%s", chainAlias))
}
// Always include the full chain ID path
bases = append(bases, fmt.Sprintf("bc/%s", chainID.String()))
return bases
}
// getFullRoutes constructs full route paths for logging.
// Clear, complete information for operators.
func (m *HandlerManager) getFullRoutes(bases []string, endpoints []string) []string {
routes := []string{}
for _, base := range bases {
for _, endpoint := range endpoints {
routes = append(routes, fmt.Sprintf("/v1/%s%s", base, endpoint))
}
}
return routes
}
// registerWithRetry attempts registration with exponential backoff.
// Handles transient failures gracefully.
func (m *HandlerManager) registerWithRetry(
ctx context.Context,
base string,
endpoint string,
handler http.Handler,
) error {
wait := m.retryWait
var lastErr error
for attempt := 0; attempt < m.retries; attempt++ {
// Check context cancellation
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// Try registration
if err := m.server.AddRoute(handler, base, endpoint); err == nil {
return nil // Success!
} else {
lastErr = err
m.log.Debug("Registration attempt failed, retrying",
log.Int("attempt", attempt+1),
log.String("base", base),
log.String("endpoint", endpoint),
log.Err(err))
}
// Don't wait after last attempt
if attempt < m.retries-1 {
select {
case <-time.After(wait):
wait *= 2 // Exponential backoff
case <-ctx.Done():
return ctx.Err()
}
}
}
return fmt.Errorf("failed after %d attempts: %w", m.retries, lastErr)
}
// healthCheckRoute performs a basic health check on a registered route.
// Validates that handlers are actually responding.
func (m *HandlerManager) healthCheckRoute(info *RouteInfo) error {
if info.Handler == nil {
return fmt.Errorf("no handler to check for chain %s", info.ChainID)
}
// Create a test request
req := httptest.NewRequest("POST", "/", strings.NewReader(`{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":1}`))
req.Header.Set("Content-Type", "application/json")
// Record the response
recorder := httptest.NewRecorder()
// Call the handler
info.Handler.ServeHTTP(recorder, req)
// Check response
info.LastCheck = time.Now()
if recorder.Code == http.StatusOK || recorder.Code == http.StatusMethodNotAllowed {
info.Healthy = true
m.log.Debug("Health check passed",
log.Stringer("chainID", info.ChainID),
log.Int("status", recorder.Code))
return nil
}
info.Healthy = false
return fmt.Errorf("%w: status %d", errHealthCheckFailed, recorder.Code)
}
// GetRouteInfo returns information about a registered chain's routes.
// Useful for debugging and monitoring.
func (m *HandlerManager) GetRouteInfo(chainID ids.ID) (*RouteInfo, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
info, exists := m.routes[chainID.String()]
return info, exists
}
// GetAllRoutes returns all registered route information.
// Complete visibility for operators.
func (m *HandlerManager) GetAllRoutes() map[string]*RouteInfo {
m.mu.RLock()
defer m.mu.RUnlock()
// Return a copy to prevent external modification
routes := make(map[string]*RouteInfo, len(m.routes))
for k, v := range m.routes {
routes[k] = v
}
return routes
}
// HealthCheckAll performs health checks on all registered routes.
// Batch operation for monitoring systems.
func (m *HandlerManager) HealthCheckAll() map[string]bool {
m.mu.RLock()
routes := make([]*RouteInfo, 0, len(m.routes))
for _, info := range m.routes {
routes = append(routes, info)
}
m.mu.RUnlock()
results := make(map[string]bool)
for _, info := range routes {
err := m.healthCheckRoute(info)
results[info.ChainID.String()] = err == nil
}
return results
}
// SetRetryConfig allows customization of retry behavior.
// Flexibility for different deployment scenarios.
func (m *HandlerManager) SetRetryConfig(maxRetries int, initialWait time.Duration) {
if maxRetries > 0 {
m.retries = maxRetries
}
if initialWait > 0 {
m.retryWait = initialWait
}
}
-291
View File
@@ -1,291 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpc
import (
"context"
"errors"
"net/http"
"testing"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/node/server/http"
"github.com/luxfi/runtime"
"github.com/luxfi/vm"
"github.com/stretchr/testify/require"
)
// mockServer implements a test server for handler registration
type mockServer struct {
routes map[string]http.Handler
failCount int
maxFailures int
returnError error
aliases map[string][]string
}
func newMockServer() *mockServer {
return &mockServer{
routes: make(map[string]http.Handler),
maxFailures: 0,
aliases: make(map[string][]string),
}
}
func (s *mockServer) AddRoute(handler http.Handler, base, endpoint string) error {
// Simulate transient failures for retry testing
if s.failCount < s.maxFailures {
s.failCount++
return errors.New("transient failure")
}
// Return configured error if any
if s.returnError != nil {
return s.returnError
}
// Store the route
key := base + endpoint
s.routes[key] = handler
return nil
}
func (s *mockServer) AddAliases(endpoint string, aliases ...string) error {
s.aliases[endpoint] = aliases
return nil
}
func (s *mockServer) AddRouteWithReadLock(handler http.Handler, base, endpoint string) error {
return s.AddRoute(handler, base, endpoint)
}
func (s *mockServer) AddAliasesWithReadLock(endpoint string, aliases ...string) error {
return s.AddAliases(endpoint, aliases...)
}
func (s *mockServer) Dispatch() error { return nil }
func (s *mockServer) RegisterChain(chainName string, rt *runtime.Runtime, vm vm.VM) {
}
func (s *mockServer) Shutdown() error { return nil }
func (s *mockServer) SetRootInfoProvider(_ server.RootInfoProvider) {}
func TestHandlerManager_RegisterChainHandlers(t *testing.T) {
tests := []struct {
name string
chainID ids.ID
chainAlias string
handlers map[string]http.Handler
serverError error
expectError bool
expectRoutes int
}{
{
name: "successful registration with alias",
chainID: ids.GenerateTestID(),
chainAlias: "C",
handlers: map[string]http.Handler{
"/rpc": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}),
"/ws": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}),
},
expectError: false,
expectRoutes: 4, // 2 endpoints × 2 bases (alias + ID)
},
{
name: "successful registration without alias",
chainID: ids.GenerateTestID(),
handlers: map[string]http.Handler{
"/rpc": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}),
},
expectError: false,
expectRoutes: 1,
},
{
name: "nil handler validation",
chainID: ids.GenerateTestID(),
chainAlias: "X",
handlers: map[string]http.Handler{"/rpc": nil},
expectError: true,
},
{
name: "empty endpoint validation",
chainID: ids.GenerateTestID(),
handlers: map[string]http.Handler{"": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})},
expectError: true,
},
{
name: "no handlers provided",
chainID: ids.GenerateTestID(),
handlers: map[string]http.Handler{},
expectError: true,
},
{
name: "invalid endpoint format",
chainID: ids.GenerateTestID(),
handlers: map[string]http.Handler{"rpc": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup
server := newMockServer()
server.returnError = tt.serverError
logger := log.NewNoOpLogger()
manager := NewHandlerManager(server, logger)
// Execute
ctx := context.Background()
err := manager.RegisterChainHandlers(ctx, tt.chainID, tt.chainAlias, tt.handlers)
// Verify
if tt.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Len(t, server.routes, tt.expectRoutes)
// Verify route info was stored
info, exists := manager.GetRouteInfo(tt.chainID)
require.True(t, exists)
require.Equal(t, tt.chainID, info.ChainID)
require.Equal(t, tt.chainAlias, info.ChainAlias)
}
})
}
}
func TestHandlerManager_RetryLogic(t *testing.T) {
// Setup server that fails twice then succeeds
server := newMockServer()
server.maxFailures = 2
logger := log.NewNoOpLogger()
manager := NewHandlerManager(server, logger)
manager.SetRetryConfig(3, 10*time.Millisecond)
// Create test handler
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
// Register with retries
ctx := context.Background()
chainID := ids.GenerateTestID()
require.NoError(t, manager.RegisterChainHandlers(ctx, chainID, "TEST", map[string]http.Handler{
"/rpc": handler,
}))
require.Equal(t, 2, server.failCount) // Failed twice, succeeded on third try
require.Len(t, server.routes, 2) // Both alias and ID routes
}
func TestHandlerManager_HealthCheck(t *testing.T) {
server := newMockServer()
logger := log.NewNoOpLogger()
manager := NewHandlerManager(server, logger)
// Register a healthy handler
healthyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"jsonrpc":"2.0","result":"test","id":1}`))
})
// Register an unhealthy handler
unhealthyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
})
ctx := context.Background()
chainID1 := ids.GenerateTestID()
chainID2 := ids.GenerateTestID()
// Register healthy chain
require.NoError(t, manager.RegisterChainHandlers(ctx, chainID1, "", map[string]http.Handler{
"/rpc": healthyHandler,
}))
// Register unhealthy chain
// Registration succeeds even if health check fails
require.NoError(t, manager.RegisterChainHandlers(ctx, chainID2, "", map[string]http.Handler{
"/rpc": unhealthyHandler,
})) // Registration should succeed regardless of handler health
// Check health status
results := manager.HealthCheckAll()
require.True(t, results[chainID1.String()])
require.False(t, results[chainID2.String()])
}
func TestHandlerManager_GetBasePaths(t *testing.T) {
manager := &HandlerManager{}
chainID := ids.GenerateTestID()
// Test with alias
bases := manager.getBasePaths(chainID, "C")
require.Equal(t, []string{"bc/C", "bc/" + chainID.String()}, bases)
// Test without alias
bases = manager.getBasePaths(chainID, "")
require.Equal(t, []string{"bc/" + chainID.String()}, bases)
// Test when alias equals chain ID (shouldn't duplicate)
bases = manager.getBasePaths(chainID, chainID.String())
require.Equal(t, []string{"bc/" + chainID.String()}, bases)
}
func TestHandlerManager_ContextCancellation(t *testing.T) {
// Create a server that delays to test cancellation
server := &mockServer{
routes: make(map[string]http.Handler),
returnError: errors.New("slow server"),
}
logger := log.NewNoOpLogger()
manager := NewHandlerManager(server, logger)
manager.SetRetryConfig(10, 100*time.Millisecond) // Many retries with delays
// Create cancelled context
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
// Try to register - should fail with context error
chainID := ids.GenerateTestID()
err := manager.RegisterChainHandlers(ctx, chainID, "", map[string]http.Handler{
"/rpc": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}),
})
require.Error(t, err)
require.Contains(t, err.Error(), "context canceled")
}
// Benchmark to ensure performance doesn't degrade
func BenchmarkHandlerRegistration(b *testing.B) {
server := newMockServer()
logger := log.NewNoOpLogger()
manager := NewHandlerManager(server, logger)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
ctx := context.Background()
b.ResetTimer()
for i := 0; i < b.N; i++ {
chainID := ids.GenerateTestID()
manager.RegisterChainHandlers(ctx, chainID, "TEST", map[string]http.Handler{
"/rpc": handler,
"/ws": handler,
})
}
}
-165
View File
@@ -1,165 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpc
import (
"context"
"fmt"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/node/server/http"
)
// IntegrationExample shows how to modify the existing createChain function in manager.go.
// This replaces lines 941-990 with cleaner, more robust code.
func IntegrationExample(
ctx context.Context,
chainID ids.ID,
vm interface{},
server server.Server,
logger log.Logger,
cChainID ids.ID,
pChainID ids.ID,
isDevMode bool,
) error {
// BEFORE: 50+ lines of complex type checking and error-prone registration
// AFTER: Clean, robust registration with proper error handling
// Step 1: Create the registrar
registrar := NewChainHandlerRegistrar(server, logger, cChainID, pChainID)
// Step 2: Configure based on environment
if isDevMode {
// Development: Fast failures for quick iteration
registrar.SetRetryConfig(2, 50*time.Millisecond)
logger.Info("Using development handler registration settings")
} else {
// Production: More robust with retries
registrar.SetRetryConfig(5, 200*time.Millisecond)
logger.Info("Using production handler registration settings")
}
// Step 3: Register handlers (replaces all the complex VM type checking)
startTime := time.Now()
err := registrar.RegisterChainHandlers(ctx, chainID, vm)
duration := time.Since(startTime)
// Step 4: Handle registration result
if err != nil {
// Log error but don't fail chain creation
// Handlers are not critical for chain operation
logger.Error("RPC handler registration failed",
log.Stringer("chainID", chainID),
log.Err(err),
log.Duration("duration", duration),
log.String("action", "Chain will operate without HTTP/RPC access"))
// Could emit metrics here if available
// metric.HandlerRegistrationFailed.Inc()
// Non-fatal: return nil to allow chain to continue
// Change to 'return err' if you want this to be fatal
return nil
}
// Step 5: Log success with useful information
if info, exists := registrar.GetRouteInfo(chainID); exists {
logger.Info("RPC handlers registered successfully",
log.Stringer("chainID", chainID),
log.String("alias", info.ChainAlias),
log.Strings("endpoints", info.Endpoints),
log.Duration("duration", duration),
log.Bool("healthCheckPassed", info.Healthy))
// Print developer-friendly message
if isDevMode && len(info.Endpoints) > 0 {
baseURL := "http://localhost:9630"
fmt.Printf("\n✅ Chain %s RPC endpoints ready:\n", chainID)
for _, endpoint := range info.Endpoints {
if info.ChainAlias != "" {
fmt.Printf(" %s/v1/bc/%s%s\n", baseURL, info.ChainAlias, endpoint)
}
fmt.Printf(" %s/v1/bc/%s%s\n", baseURL, chainID, endpoint)
}
fmt.Println()
}
}
// Step 6: Schedule async health monitoring (optional)
if !isDevMode {
go monitorHandlerHealth(ctx, registrar, chainID, logger)
}
return nil
}
// monitorHandlerHealth runs periodic health checks in the background.
// This helps detect and log handler issues early.
func monitorHandlerHealth(
ctx context.Context,
registrar *ChainHandlerRegistrar,
chainID ids.ID,
logger log.Logger,
) {
// Initial delay to let chain fully initialize
select {
case <-time.After(10 * time.Second):
case <-ctx.Done():
return
}
// Run initial health check
checkHealth(registrar, chainID, logger)
// Periodic health checks
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
checkHealth(registrar, chainID, logger)
case <-ctx.Done():
return
}
}
}
// checkHealth performs a health check and logs results.
func checkHealth(registrar *ChainHandlerRegistrar, chainID ids.ID, logger log.Logger) {
results := registrar.HealthCheckAll()
for chainIDStr, healthy := range results {
if chainIDStr == chainID.String() {
if healthy {
logger.Debug("Handler health check passed",
log.String("chainID", chainIDStr))
} else {
logger.Warn("Handler health check failed",
log.String("chainID", chainIDStr),
log.String("action", "Will continue monitoring"))
}
}
}
}
// MinimalIntegration shows the absolute minimum code needed.
// This is what you'd actually put in manager.go.
func MinimalIntegration(
ctx context.Context,
chainID ids.ID,
vm interface{},
server server.Server,
logger log.Logger,
cChainID ids.ID,
) error {
// Just three lines to replace 50+ lines of complex code!
registrar := NewChainHandlerRegistrar(server, logger, cChainID, ids.Empty)
if err := registrar.RegisterChainHandlers(ctx, chainID, vm); err != nil {
logger.Error("Handler registration failed", log.Err(err))
}
return nil // Non-fatal
}
+8
View File
@@ -1762,6 +1762,14 @@ func GetNodeConfig(v *viper.Viper) (node.Config, error) {
if nodeConfig.HealthCheckFreq < 0 {
return node.Config{}, fmt.Errorf("%s must be positive", HealthCheckFreqKey)
}
nodeConfig.ProposerWindowDuration = v.GetDuration(ProposerVMWindowDurationKey)
if nodeConfig.ProposerWindowDuration < 0 {
return node.Config{}, fmt.Errorf("%s must not be negative", ProposerVMWindowDurationKey)
}
nodeConfig.ProposerMinBlockDelay = v.GetDuration(ProposerVMMinBlockDelayKey)
if nodeConfig.ProposerMinBlockDelay < 0 {
return node.Config{}, fmt.Errorf("%s must not be negative", ProposerVMMinBlockDelayKey)
}
// Halflife of continuous averager used in health checks
healthCheckAveragerHalflife := v.GetDuration(HealthCheckAveragerHalflifeKey)
if healthCheckAveragerHalflife <= 0 {
+1
View File
@@ -369,6 +369,7 @@ func addNodeFlags(fs *pflag.FlagSet) {
// ProposerVM
fs.Bool(ProposerVMUseCurrentHeightKey, false, "Have the ProposerVM always report the last accepted P-chain block height")
fs.Duration(ProposerVMMinBlockDelayKey, proposervm.DefaultMinBlockDelay, "Minimum delay to enforce when building a chain++ block for the primary network chains and the default minimum delay for chains")
fs.Duration(ProposerVMWindowDurationKey, 0, "Proposer-slot spacing for block production (0 uses the 5s mainnet default); shrink for fast cadence on small/local networks")
// Metrics
fs.Bool(MeterVMsEnabledKey, true, "Enable Meter VMs to track VM performance with more granularity")
+1
View File
@@ -187,6 +187,7 @@ const (
ConsensusFrontierPollFrequencyKey = "consensus-frontier-poll-frequency"
ProposerVMUseCurrentHeightKey = "proposervm-use-current-height"
ProposerVMMinBlockDelayKey = "proposervm-min-block-delay"
ProposerVMWindowDurationKey = "proposervm-window-duration"
FdLimitKey = "fd-limit"
IndexEnabledKey = "index-enabled"
IndexAllowIncompleteKey = "index-allow-incomplete"
+10
View File
@@ -177,6 +177,16 @@ type Config struct {
// Health
HealthCheckFreq time.Duration `json:"healthCheckFreq"`
// ProposerWindowDuration overrides the proposervm proposer-slot spacing.
// Zero keeps the 5s mainnet default; small local/dev nets set it low (e.g.
// 1s) so block cadence is not floored at 5s per proposer slot.
ProposerWindowDuration time.Duration `json:"proposerWindowDuration"`
// ProposerMinBlockDelay is the proposervm minimum delay between consecutive
// blocks (the hard cadence floor). Zero keeps the 1s default; high-throughput
// / DEX nets set it low (e.g. 1ms) to approach the consensus-finality floor.
ProposerMinBlockDelay time.Duration `json:"proposerMinBlockDelay"`
// Network configuration
NetworkConfig network.Config `json:"networkConfig"`
BIN
View File
Binary file not shown.
+173
View File
@@ -0,0 +1,173 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package builder
import (
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/luxfi/constants"
"github.com/luxfi/node/vms/platformvm/genesis"
pchaintxs "github.com/luxfi/node/vms/platformvm/txs"
)
// parsePolicy reads the operator form "3-of-5" into (K, N).
//
// The canonical parser is luxfi/threshold/pkg/quorum, but the node's genesis
// builder is a boot-critical path and does not take a dependency on a
// threshold-crypto library to read three integers. What this test pins is the
// FORM — that the genesis blob states a quorum in a spelling that cannot be
// confused with a polynomial degree — and the form is checkable here.
func parsePolicy(t *testing.T, s string) (k, n int) {
t.Helper()
_, err := fmt.Sscanf(s, "%d-of-%d", &k, &n)
require.NoErrorf(t, err, "policy %q is not in the operator form \"3-of-5\"", s)
require.Greaterf(t, k, 1, "policy %q: k=1 is not a threshold policy", s)
require.LessOrEqualf(t, k, n, "policy %q can never be satisfied", s)
return k, n
}
// canonicalMPCVMID is M-Chain's vmID in the encoding the node's plugin registry
// resolves against. Pinned literally: the plugin binary's FILENAME must equal
// it, and a change that leaves the two out of step produces a chain that is
// declared in genesis but that no node can start.
//
// The predecessor value, tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t, is
// the retired `thresholdvm` identifier. It was what the Dockerfile installed
// the plugin under while genesis declared MPCVMID, so the two never met.
const canonicalMPCVMID = "qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS"
// M-Chain must be a GENESIS chain — in the P-Chain's chain set at height 0,
// tracked by every validator from boot — not a chain created later by a
// CreateChainTx someone has to remember to submit.
//
// The difference matters for custody specifically. A post-genesis chain exists
// only from the height its tx landed, so a node syncing from genesis has a
// window with no custody state, and the chain's very existence depends on an
// operator action that can be forgotten or fumbled. Bridged funds should not
// depend on that.
func TestMChainIsAGenesisChain(t *testing.T) {
for _, networkID := range []uint32{
constants.MainnetID,
constants.TestnetID,
constants.LocalID,
} {
cfg := GetConfig(networkID)
require.NotNilf(t, cfg, "network %d has no genesis config", networkID)
require.NotEmptyf(t, cfg.MChainGenesis,
"network %d carries no mchain.json, so the builder skips M-Chain entirely", networkID)
raw, _, err := FromConfig(cfg)
require.NoErrorf(t, err, "network %d genesis build", networkID)
parsed, err := genesis.Parse(raw)
require.NoErrorf(t, err, "network %d genesis parse", networkID)
// Genesis chains are CreateChainTx entries executed at height 0 — the
// chain exists from the first block, without anyone submitting a tx.
var found *pchaintxs.CreateChainTx
for _, c := range parsed.Chains {
u, ok := c.Unsigned.(*pchaintxs.CreateChainTx)
require.True(t, ok, "a genesis chain entry must be a CreateChainTx")
if u.VMID() == constants.MPCVMID {
found = u
break
}
}
require.NotNilf(t, found,
"network %d: no genesis chain with vmID %s (M-Chain)", networkID, constants.MPCVMID)
require.Equal(t, "M-Chain", found.BlockchainName())
require.Equalf(t, []byte(cfg.MChainGenesis), found.GenesisData(),
"network %d: the VM must receive exactly the mchain.json bytes", networkID)
require.Equalf(t, canonicalMPCVMID, found.VMID().String(),
"network %d: genesis declares a vmID the plugin registry will look up by filename", networkID)
}
}
// The genesis blob decides how many custodians must cooperate to move bridged
// funds, so it must state that in a form nothing can misread as a polynomial
// degree. A bare `"threshold": 3` reads as the signer count to an operator and
// as the degree to every threshold library, and those differ by one.
func TestMChainGenesisPolicyIsUnambiguousAndDeployable(t *testing.T) {
for _, tc := range []struct {
networkID uint32
want string
}{
{constants.MainnetID, "3-of-5"},
{constants.TestnetID, "3-of-5"},
{constants.LocalID, "2-of-3"},
} {
cfg := GetConfig(tc.networkID)
require.NotNil(t, cfg)
var blob struct {
Policy string `json:"policy"`
VM string `json:"vm"`
}
require.NoErrorf(t, json.Unmarshal([]byte(cfg.MChainGenesis), &blob),
"network %d mchain.json must decode, policy included", tc.networkID)
require.Equalf(t, tc.want, blob.Policy, "network %d policy", tc.networkID)
k, n := parsePolicy(t, blob.Policy)
require.Greaterf(t, 2*k, n,
"network %d: %s admits two disjoint quorums, so two halves of the committee could authorise contradictory releases",
tc.networkID, blob.Policy)
require.Equalf(t, "mpcvm", blob.VM,
"network %d must name the current VM, not the retired ThresholdVM", tc.networkID)
}
}
// The ambiguous fields must be gone, not merely ignored. Leaving them in place
// means the next reader has two numbers to choose between, and the whole point
// of the policy field is that there is exactly one.
func TestMChainGenesisHasNoAmbiguousThresholdFields(t *testing.T) {
for _, networkID := range []uint32{constants.MainnetID, constants.TestnetID, constants.LocalID} {
cfg := GetConfig(networkID)
require.NotNil(t, cfg)
var blob map[string]any
require.NoError(t, json.Unmarshal([]byte(cfg.MChainGenesis), &blob))
for _, dead := range []string{"mpcThreshold", "mpcParties", "threshold", "totalParties"} {
require.NotContainsf(t, blob, dead,
"network %d mchain.json still carries %q; the policy field is the only quorum statement",
networkID, dead)
}
}
}
// A policy is only real if the network has enough validators to hold its
// shares. M-Chain draws its committee from the validator set, and RunKeygen
// refuses a policy needing more parties than the committee has — so a genesis
// declaring 7-of-10 on a five-validator network fails closed forever: no
// custody key can ever be generated, and the failure only shows up the first
// time someone tries to bridge.
//
// mainnet's mchain.json shipped exactly that mismatch for as long as the field
// existed, harmlessly, because nothing read it. This test is what keeps it
// harmless now that the policy is authoritative.
func TestMChainPolicyIsSatisfiableByTheGenesisValidatorSet(t *testing.T) {
for _, networkID := range []uint32{constants.MainnetID, constants.TestnetID, constants.LocalID} {
cfg := GetConfig(networkID)
require.NotNil(t, cfg)
raw, _, err := FromConfig(cfg)
require.NoError(t, err)
parsed, err := genesis.Parse(raw)
require.NoError(t, err)
var blob struct {
Policy string `json:"policy"`
}
require.NoError(t, json.Unmarshal([]byte(cfg.MChainGenesis), &blob))
_, n := parsePolicy(t, blob.Policy)
require.LessOrEqualf(t, n, len(parsed.Validators),
"network %d: M-Chain policy %s needs %d parties but genesis declares %d validators; "+
"keygen would fail closed and no custody key could ever exist",
networkID, blob.Policy, n, len(parsed.Validators))
}
}
+72 -68
View File
@@ -26,21 +26,21 @@ require (
github.com/huin/goupnp v1.3.0
github.com/jackpal/gateway v1.1.1
github.com/jackpal/go-nat-pmp v1.0.2
github.com/luxfi/consensus v1.36.1
github.com/luxfi/crypto v1.20.0
github.com/luxfi/database v1.20.4
github.com/luxfi/ids v1.3.1
github.com/luxfi/keychain v1.0.2
github.com/luxfi/consensus v1.36.12
github.com/luxfi/crypto v1.20.2
github.com/luxfi/database v1.21.1
github.com/luxfi/ids v1.3.2
github.com/luxfi/keychain v1.1.1
github.com/luxfi/log v1.4.3
github.com/luxfi/math v1.4.1
github.com/luxfi/metric v1.6.0
github.com/luxfi/math v1.5.1
github.com/luxfi/metric v1.8.1
github.com/luxfi/mock v0.1.1
github.com/mr-tron/base58 v1.3.0
github.com/onsi/ginkgo/v2 v2.28.1
github.com/pires/go-proxyproto v0.11.0
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/common v0.68.0 // indirect
github.com/rs/cors v1.11.1
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/spf13/cast v1.10.0 // indirect
@@ -56,13 +56,13 @@ require (
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.uber.org/goleak v1.3.0
go.uber.org/mock v0.6.0
golang.org/x/crypto v0.52.0
golang.org/x/crypto v0.54.0
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9 // indirect
golang.org/x/mod v0.36.0
golang.org/x/net v0.55.0
golang.org/x/sync v0.20.0
golang.org/x/mod v0.37.0
golang.org/x/net v0.57.0
golang.org/x/sync v0.22.0
golang.org/x/time v0.15.0
golang.org/x/tools v0.45.0
golang.org/x/tools v0.47.0
gonum.org/v1/gonum v0.17.0
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
@@ -73,7 +73,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/cespare/xxhash/v2 v2.3.0
github.com/cockroachdb/errors v1.12.0 // indirect
github.com/cockroachdb/errors v1.13.0 // indirect
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect
github.com/cockroachdb/redact v1.1.8 // indirect
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb // indirect
@@ -81,7 +81,7 @@ require (
github.com/deckarep/golang-set/v2 v2.9.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/getsentry/sentry-go v0.44.1 // indirect
github.com/getsentry/sentry-go v0.46.2 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
@@ -90,7 +90,7 @@ require (
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.18.6
github.com/klauspost/compress v1.19.1
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
@@ -98,7 +98,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/rogpeppe/go-internal v1.15.0 // indirect
github.com/sanity-io/litter v1.5.5 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
@@ -107,8 +107,8 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.40.0 // indirect
)
require (
@@ -118,60 +118,61 @@ require (
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/golang/mock v1.7.0-rc.1
github.com/luxfi/accel v1.2.4
github.com/luxfi/api v1.0.16
github.com/luxfi/api v1.1.1
github.com/luxfi/atomic v1.0.0
github.com/luxfi/chains v1.7.6
github.com/luxfi/codec v1.1.5
github.com/luxfi/compress v0.0.5
github.com/luxfi/constants v1.6.1
github.com/luxfi/container v0.0.4
github.com/luxfi/chains v1.7.9
github.com/luxfi/codec v1.2.1
github.com/luxfi/compress v0.1.1
github.com/luxfi/constants v1.6.2
github.com/luxfi/container v0.2.1
github.com/luxfi/filesystem v0.0.1
github.com/luxfi/genesis v1.16.1
github.com/luxfi/genesis v1.16.4
github.com/luxfi/genesis/pkg/genesis/security v1.13.8
github.com/luxfi/geth v1.17.12
github.com/luxfi/go-bip39 v1.1.2
github.com/luxfi/keys v1.2.0
github.com/luxfi/geth v1.20.1
github.com/luxfi/go-bip39 v1.2.0
github.com/luxfi/kms v1.12.10
github.com/luxfi/math/safe v0.0.1
github.com/luxfi/net v0.0.5
github.com/luxfi/p2p v1.21.1
github.com/luxfi/resource v0.0.1
github.com/luxfi/net v0.1.1
github.com/luxfi/p2p v1.22.1
github.com/luxfi/resource v0.1.1
github.com/luxfi/rpc v1.1.0
github.com/luxfi/runtime v1.1.3
github.com/luxfi/sdk v1.17.9
github.com/luxfi/runtime v1.3.1
github.com/luxfi/sdk v1.18.1
github.com/luxfi/sys v0.1.0
github.com/luxfi/threshold v1.12.1
github.com/luxfi/timer v1.0.2
github.com/luxfi/threshold v1.12.3
github.com/luxfi/timer v1.1.1
github.com/luxfi/units v1.0.0
github.com/luxfi/utils v1.2.0
github.com/luxfi/utxo v0.5.7
github.com/luxfi/validators v1.2.0
github.com/luxfi/vm v1.2.7
github.com/luxfi/warp v1.24.0
github.com/luxfi/zap v1.2.5
github.com/luxfi/zwing v0.5.2
github.com/luxfi/utils v1.3.1
github.com/luxfi/utxo v0.5.8
github.com/luxfi/validators v1.3.1
github.com/luxfi/vm v1.3.3
github.com/luxfi/warp v1.24.1
github.com/luxfi/zap v1.2.6
github.com/luxfi/zwing v0.6.1
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354
github.com/zap-proto/http v0.0.0-20260506200741-fd6047874433
github.com/valyala/fasthttp v1.73.0
github.com/zap-proto/http v0.3.0
go.uber.org/zap v1.27.1
)
require (
capnproto.org/go/capnp/v3 v3.0.1-alpha.2 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
filippo.io/hpke v0.4.0 // indirect
github.com/andybalholm/brotli v1.2.2 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.13 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.13 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.14 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18 // indirect
@@ -180,7 +181,6 @@ require (
github.com/btcsuite/btcd/btcec/v2 v2.5.0 // indirect
github.com/btcsuite/btcd/chainhash/v2 v2.0.0 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/colega/zeropool v0.0.0-20230505084239-6fb4a4f75381 // indirect
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
@@ -191,20 +191,23 @@ require (
github.com/hanzos3/go-sdk v1.0.2 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/luxfi/age v1.6.0 // indirect
github.com/luxfi/corona v0.10.3 // indirect
github.com/luxfi/bft v0.1.5 // indirect
github.com/luxfi/corona v0.10.4 // indirect
github.com/luxfi/crypto/ipa v1.2.4 // indirect
github.com/luxfi/dkg v0.3.5 // indirect
github.com/luxfi/kms v1.11.7 // indirect
github.com/luxfi/keys v1.4.1 // indirect
github.com/luxfi/lattice/v7 v7.1.4 // indirect
github.com/luxfi/lens v0.1.4 // indirect
github.com/luxfi/lens v0.2.1 // indirect
github.com/luxfi/light v1.0.0 // indirect
github.com/luxfi/magnetar v1.2.3 // indirect
github.com/luxfi/mdns v0.1.1 // indirect
github.com/luxfi/mlwe v0.2.1 // indirect
github.com/luxfi/mlwe v0.3.0 // indirect
github.com/luxfi/pq v1.1.0 // indirect
github.com/luxfi/precompile v0.19.1 // indirect
github.com/luxfi/pulsar v1.9.0 // indirect
github.com/luxfi/staking v1.5.1 // indirect
github.com/luxfi/trace v1.1.0 // indirect
github.com/luxfi/precompile v0.19.3 // indirect
github.com/luxfi/protocol v0.0.2 // indirect
github.com/luxfi/pulsar v1.9.2 // indirect
github.com/luxfi/staking v1.6.1 // indirect
github.com/luxfi/trace v1.2.1 // indirect
github.com/luxfi/zapdb v1.10.1 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect
@@ -213,14 +216,16 @@ require (
github.com/philhofer/fwd v1.2.0 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/tinylib/msgp v1.6.4 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/zap-proto/go v1.1.0 // indirect
go.mongodb.org/mongo-driver v1.17.9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
)
require (
github.com/luxfi/concurrent v0.0.3
github.com/luxfi/proto v1.3.5
github.com/luxfi/upgrade v1.0.1 // indirect
github.com/luxfi/concurrent v0.1.1
github.com/luxfi/proto v1.4.2
github.com/luxfi/upgrade v1.0.3 // indirect
github.com/luxfi/version v1.0.1
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
)
@@ -241,13 +246,13 @@ require (
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/luxfi/address v1.0.1
github.com/luxfi/cache v1.2.1 // indirect
github.com/luxfi/formatting v1.0.1
github.com/luxfi/go-bip32 v1.0.2
github.com/luxfi/address v1.1.1
github.com/luxfi/cache v1.3.1 // indirect
github.com/luxfi/formatting v1.1.1
github.com/luxfi/go-bip32 v1.1.0
github.com/luxfi/math/big v0.1.0 // indirect
github.com/luxfi/sampler v1.1.0 // indirect
github.com/luxfi/tls v1.0.3 // indirect
github.com/luxfi/tls v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/montanaflynn/stats v0.9.0 // indirect
@@ -256,7 +261,6 @@ require (
github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
)
+151 -321
View File
@@ -1,47 +1,30 @@
bazil.org/fuse v0.0.0-20230120002735-62a210ff1fd5/go.mod h1:gG3RZAMXCa/OTes6rr9EwusmR1OH1tDDy+cg9c5YliY=
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAwLGRZ+sebTUruuM4Ra3M=
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
capnproto.org/go/capnp/v3 v3.0.1-alpha.2 h1:W/cf+XEArUSwcBBE/9wS2NpWDkM5NLQOjmzEiHZpYi0=
capnproto.org/go/capnp/v3 v3.0.1-alpha.2/go.mod h1:2vT5D2dtG8sJGEoEKU17e+j7shdaYp1Myl8X03B3hmc=
codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU=
codeberg.org/go-latex/latex v0.1.0/go.mod h1:LA0q/AyWIYrqVd+A9Upkgsb+IqPcmSTKc9Dny04MHMw=
codeberg.org/go-pdf/fpdf v0.10.0/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU=
connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
filippo.io/nistec v0.0.4/go.mod h1:PK/lw8I1gQT4hUML4QGaqljwdDaFcMyFKSXN7kjrtKI=
git.sr.ht/~sbinet/gg v0.6.0/go.mod h1:uucygbfC9wVPQIfrmwM2et0imr8L7KQWywX0xpFMm94=
github.com/ALTree/bigfloat v0.2.0 h1:AwNzawrpFuw55/YDVlcPw0F0cmmXrmngBHhVrvdXPvM=
github.com/ALTree/bigfloat v0.2.0/go.mod h1:+NaH2gLeY6RPBPPQf4aRotPPStg+eXc8f9ZaE4vRfD4=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4/go.mod h1:8mwH4klAm9DUgR2EEHyEEAQlRDvLPyg5fQry3y+cDew=
github.com/ChainSafe/go-schnorrkel v1.1.0 h1:rZ6EU+CZFCjB4sHUE1jIu8VDoB/wRKZxoe1tkcO71Wk=
github.com/ChainSafe/go-schnorrkel v1.1.0/go.mod h1:ABkENxiP+cvjFiByMIZ9LYbRoNNLeBLiakC1XeTFxfE=
github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260311194731-d5b7577c683d/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/StephenButtolph/canoto v0.17.3 h1:lvsnYD4b96vD1knnmp1xCmZqfYpY/jSeRozGdOfdvGI=
github.com/StephenButtolph/canoto v0.17.3/go.mod h1:IcnAHC6nJUfQFVR9y60ko2ecUqqHHSB6UwI9NnBFZnE=
github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794/go.mod h1:7e+I0LQFUI9AXWxOfsQROs9xPhoJtbsyWcjJqDd4KPY=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
github.com/aws/aws-sdk-go-v2/config v1.32.13 h1:5KgbxMaS2coSWRrx9TX/QtWbqzgQkOdEa3sZPhBhCSg=
github.com/aws/aws-sdk-go-v2/config v1.32.13/go.mod h1:8zz7wedqtCbw5e9Mi2doEwDyEgHcEE9YOJp6a8jdSMY=
github.com/aws/aws-sdk-go-v2/credentials v1.19.13 h1:mA59E3fokBvyEGHKFdnpNNrvaR351cqiHgRg+JzOSRI=
@@ -56,17 +39,24 @@ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 h1:fJvQ5mIBVfKtiyx0AHY6HeWcRX5LGANLpq8SVR+Uazs=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY=
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.5/go.mod h1:TmxGowuBYwjmHFOsEDxaZdsQE62JJzOmtiWafTi/czg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 h1:M1A9AjcFwlxTLuf0Faj88L8Iqw0n/AJHjpZTQzMMsSc=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2/go.mod h1:KsdTV6Q9WKUZm2mNJnUFmIoXfZux91M3sr/a4REX8e0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9/go.mod h1:7yuQJoT+OoH8aqIxw9vwF+8KpvLZ8AWmvmUWHsGQZvI=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.14 h1:GcLE9ba5ehAQma6wlopUesYg/hbcOhFNWTjELkiWkh4=
@@ -101,7 +91,6 @@ github.com/btcsuite/btcd/chainhash/v2 v2.0.0 h1:PMLlSloHJuEeB80XG9EjpXWNEKAZAMLl
github.com/btcsuite/btcd/chainhash/v2 v2.0.0/go.mod h1:mKxcZ7oGTXE7IRV+sS9hP4EVBwc/SzfNR+52IsOP9j8=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
@@ -109,28 +98,19 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8=
github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chelnak/ysmrr v0.6.0/go.mod h1:56JSrmQgb7/7xoMvuD87h3PE/qW6K1+BQcrgWtVLTUo=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cloudflare/cloudflare-go v0.116.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo=
github.com/cockroachdb/errors v1.12.0/go.mod h1:SvzfYNNBshAVbZ8wzNc/UPK3w1vf0dKDUP41ucAIf7g=
github.com/cockroachdb/errors v1.13.0 h1:BoCcJeiP9hpBJDETkX19qi8Tb8So37srSsp3stTaDMQ=
github.com/cockroachdb/errors v1.13.0/go.mod h1:bjxt/4E5+OyuAnacpTIU9rn2mzPu1VlthvHP+xpROq0=
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 h1:pU88SPhIFid6/k0egdR5V6eALQYq2qbSmukrkgIh/0A=
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 h1:ASDL+UJcILMqgNeV5jiqR4j+sTuvQNHdf2chuKj1M5k=
@@ -141,10 +121,6 @@ github.com/cockroachdb/redact v1.1.8 h1:8eVLLj6juKxiKrAEw2b8cJvNqWq++U8WOfQFuL7K
github.com/cockroachdb/redact v1.1.8/go.mod h1:GceHHpJ0rMDpYARL5In88Alq/xMBUtVlz7Qxix6ZVkw=
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb h1:3bCgBvB8PbJVMX1ouCcSIxvsqKPYM7gs72o0zC76n9g=
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/colega/zeropool v0.0.0-20230505084239-6fb4a4f75381 h1:d5EKgQfRQvO97jnISfR89AiCCCJMwMFoSxUiU0OGCRU=
github.com/colega/zeropool v0.0.0-20230505084239-6fb4a4f75381/go.mod h1:OU76gHeRo8xrzGJU3F3I1CqX1ekM8dfJw0+wPeMwnp0=
github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs=
github.com/consensys/gnark-crypto v0.20.1 h1:PXDUBvk8AzhvWowHLWBEAfUQcV1/aZgWIqD6eMpXmDg=
github.com/consensys/gnark-crypto v0.20.1/go.mod h1:RBWrSgy+IDbGR69RRV313th3M/aZU1ubk2om+qHuTSc=
github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY=
@@ -154,11 +130,9 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc=
github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
github.com/crate-crypto/go-kzg-4844 v1.1.0/go.mod h1:JolLjpSff1tCCJKaJx4psrlEdlXuJEC996PL3tTAFks=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -176,30 +150,22 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/dgraph-io/badger/v4 v4.9.0/go.mod h1:5/MEx97uzdPUHR4KtkNt8asfI2T4JiEiQlV7kWUo8c0=
github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU=
github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0/go.mod h1:56wL82FO0bfMU5RvfXoIwSOP2ggqqxT+tAfNEIyxuHw=
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emicklei/dot v1.11.0 h1:zsrhCuFHAJge/aZIC4N4LdHy5tqYu4tWEaUzIwdYj4Y=
github.com/emicklei/dot v1.11.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/ethereum/c-kzg-4844/v2 v2.1.7 h1:aat3CuITdDbPC6pmEGRT0zJ5eOxzrZj8TJT5z7Xk//M=
github.com/ethereum/c-kzg-4844/v2 v2.1.7/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw=
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk=
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/ferranbt/fastssz v1.0.0 h1:9EXXYsracSqQRBQiHeaVsG/KQeYblPf40hsQPb9Dzk8=
github.com/ferranbt/fastssz v1.0.0/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
github.com/fjl/gencodec v0.1.1/go.mod h1:chDHL3wKXuBgauP8x3XNZkl5EIAR5SoCTmmmDTZRzmw=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -209,11 +175,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61/go.mod h1:Q0X6pkwTILDlzrGEckF6HKjXe48EgsY/l7K7vhY4MW8=
github.com/gballet/go-libpcsclite v0.0.0-20250918194357-1ec6f2e601c6/go.mod h1:3IVE7v4II2gS2V5amIH7F7NeYQtbbORtQtjdflgS1vk=
github.com/getsentry/sentry-go v0.44.1 h1:/cPtrA5qB7uMRrhgSn9TYtcEF36auGP3Y6+ThvD/yaI=
github.com/getsentry/sentry-go v0.44.1/go.mod h1:XDotiNZbgf5U8bPDUAfvcFmOnMQQceESxyKaObSssW0=
github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9/go.mod h1:106OIgooyS7OzLDOpUGgm9fA3bQENb/cFSyyBmMoJDs=
github.com/getsentry/sentry-go v0.46.2 h1:1jhYwrKGa3sIpo/y5iDNXS5wDoT7I1KNzMHrnK6ojns=
github.com/getsentry/sentry-go v0.46.2/go.mod h1:evVbw2qotNUdYG8KxXbAdjOQWWvWIwKxpjdZZIvcIPw=
github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
@@ -222,12 +185,8 @@ github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01
github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-json-experiment/json v0.0.0-20260601182631-00ed12fed2a6 h1:nxP4pPoyqOAgX8lYDFCfl3DyKeXErCvSvhcyzwGV9CE=
github.com/go-json-experiment/json v0.0.0-20260601182631-00ed12fed2a6/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -238,30 +197,19 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccmack/gocc v1.0.2/go.mod h1:LXX2tFVUggS/Zgx/ICPOr3MLyusuM7EcbfkPvNsjdO8=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -273,7 +221,6 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@@ -281,14 +228,12 @@ github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
@@ -307,13 +252,10 @@ github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5T
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
github.com/graph-gophers/graphql-go v1.9.0/go.mod h1:23olKZ7duEvHlF/2ELEoSZaY1aNPfShjP782SOoNTyM=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is=
github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
github.com/gtank/ristretto255 v0.2.0 h1:LeOuWr6giplWkkMizx2emfG03SRPJqKt1nfIHLVHQ/0=
github.com/gtank/ristretto255 v0.2.0/go.mod h1:OJ1ox/dWcp7sJ5grYDcZ+kkHYuj5nelW5aaL7ESVXBw=
github.com/guptarohit/asciigraph v0.5.5/go.mod h1:dYl5wwK4gNsnFf9Zp+l06rFiDZ5YtXM6x7SRWZ3KGag=
github.com/hanzoai/vfs v0.4.3 h1:QN9SemEQBq9x1l/toi51/TZWctbt3i3mgUJfz5RPALY=
github.com/hanzoai/vfs v0.4.3/go.mod h1:wTHfTpJ/165yz0qfPBNFcYRg+tGw8YDwPu+xgps88zU=
github.com/hanzos3/go-sdk v1.0.2 h1:EOJQGVnwclkzIyRJyWqtqmA2muyaSsF4y+7KYC4Vhdw=
@@ -331,46 +273,28 @@ github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXei
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
github.com/hydrogen18/memlistener v1.0.0/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/influxdata/influxdb-client-go/v2 v2.14.0/go.mod h1:Ahpm3QXKMJslpXl3IftVLVezreAUtBOTZssDrjZEFHI=
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
github.com/jackpal/gateway v1.1.1 h1:UXXXkJGIHFsStms9ZBgGpoaFEJP7oJtFn5vplIT68E8=
github.com/jackpal/gateway v1.1.1/go.mod h1:Tl1vZVtUaXx5j6P5HFmv45alhEi4yHHLfT4PRbB7eyw=
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/jacobsa/fuse v0.0.0-20260302145937-f1ba38d60fdf/go.mod h1:fcpw1yk/suvFhB8rT9P+pst+NLboWsBLky9csooKjPc=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7/go.mod h1:BMxO138bOokdgt4UaxZiEfypcSHX0t6SIFimVP1oRfk=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b/go.mod h1:HMcgvsgd0Fjj4XXDkbjdmlbI505rUPBs6WBMYg2pXks=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kilic/bls12-381 v0.1.0/go.mod h1:vDTTHJONJ6G+P2R74EhnyotQDTliQDnFEwhdmfzw1ig=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -381,185 +305,158 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
github.com/luxfi/accel v1.2.4 h1:5VbIHyEvvfobn2zBiTFODxDw1CeqxCepZOLlvkuf9yQ=
github.com/luxfi/accel v1.2.4/go.mod h1:ISIwAX+ZfsL/S5nsP2JvfldXN6Nc+QzoWf6Jtaq+xsQ=
github.com/luxfi/address v1.0.1 h1:Sc4keyuVzBIvHr7uVeYZf2/WY9YDGUgDi/iiWenj49g=
github.com/luxfi/address v1.0.1/go.mod h1:5j3Eh66v9zvv1GbNdZwt+23krV8JlSDaRzmWZU8ZRM0=
github.com/luxfi/address v1.1.1 h1:4afWzyBWzTiZN7RenBtdMC9LIvP9L4CSBzSquwKEAgI=
github.com/luxfi/address v1.1.1/go.mod h1:KG0jUBcgoJYeieKP5jboCq9UewwDBIOus8ZCqUMVlw8=
github.com/luxfi/age v1.6.0 h1:KMD8gSOP4NVCb7NWSlRcgBZNV2xm2a+qQWPyPmiX6f4=
github.com/luxfi/age v1.6.0/go.mod h1:7cu9CIyikgyAvr5MlXFapEDQ15yBaHOSdKkK5lG04WE=
github.com/luxfi/ai v0.1.0/go.mod h1:lTuY32dGjEJUgVheYuanlt1O0jHj0AZ09sUSpRsmH6M=
github.com/luxfi/api v1.0.16 h1:RrNHafKYDzI49vHZigz+A8Kmlf60hiZZYcJD9dWfswg=
github.com/luxfi/api v1.0.16/go.mod h1:Eer59msIXMnOlFncG0XjEGH3TZML0Dd1bUu4GtB7f4Q=
github.com/luxfi/api v1.1.1 h1:CXD7m0quPmUm+Qw35TrF+E7b0Fq4qz9gHfrZ5gyrjHU=
github.com/luxfi/api v1.1.1/go.mod h1:g6J0iohVqaIj2aO1u/ZJPqjiX2tog0NM3/SBf7wJ4cA=
github.com/luxfi/atomic v1.0.0 h1:xUV60MuzRvXngaQ1sM0yVC2v4TRoLlUGkkH7M9PS4yw=
github.com/luxfi/atomic v1.0.0/go.mod h1:0G2mTlQ6TXWHICUHrUUPu1/qAiIyR4gSZ2tva9ci/bI=
github.com/luxfi/bft v0.1.5 h1:5xVLPkog4e5LTgaVlb9pgxA0EWE6tkrKwHPZVRz+RZw=
github.com/luxfi/bft v0.1.5/go.mod h1:5I8Ft8yA69xZlDe3RB0i4MgbqFKLZe65o/sha8JuKvU=
github.com/luxfi/cache v1.2.1 h1:kAzOS55/hmYeNKR+0HAKv4ma48Y6JjkI8UQeqdZ8bfI=
github.com/luxfi/cache v1.2.1/go.mod h1:co7JTxZZHpKT31Yh01LFp5aZOxmoUg157FhBLQdQHVU=
github.com/luxfi/chains v1.7.5 h1:JTtE+PBwLSaIPLg38tpJre8K8UDzimvkHYxOrX1RiKo=
github.com/luxfi/chains v1.7.5/go.mod h1:0fC2xs/pwTM7z5P9PxugOcCdOQxIXdoSdr4cIc63YgU=
github.com/luxfi/chains v1.7.6 h1:dK7o2/03dWQPpSuOFjJti+heezPhz19pTY+NcaptGb4=
github.com/luxfi/chains v1.7.6/go.mod h1:EMFvf8RaJZQysL/2/Xpmfws91j0xWjWT5TdRQdaHdtE=
github.com/luxfi/codec v1.1.5 h1:KBq8uvYm5Dy+E1heG8WBmqbqu8kstlFyE5ASBBB+C8I=
github.com/luxfi/codec v1.1.5/go.mod h1:/ugIv5iEgI+VAuPIetzxNT0eJaEjOID/mrIsgIjJh8g=
github.com/luxfi/compress v0.0.5 h1:4tEUHw5MK1bu5UOjfYCt4OKMiH7yykIgmGPRA/BfJTM=
github.com/luxfi/compress v0.0.5/go.mod h1:Cc1yxD2pfzrvpO32W2GDwLKff+CylHEvzZh2Ko8RSIU=
github.com/luxfi/concurrent v0.0.3 h1:eJyv1fhaC0jMLMw6+QS774cUmp7GK+ouMgvLCqnC7cc=
github.com/luxfi/concurrent v0.0.3/go.mod h1:Aj/FR5NpM0cB2P4Nt3+tz9+dV6V+LUW4HuMgSjwq5hw=
github.com/luxfi/config v1.1.2/go.mod h1:z6t0a5pGpQz2uDW2qJPLX5fZ/eWbpiNa51gBc63ebFk=
github.com/luxfi/consensus v1.36.1 h1:aHCUTgu0SaJXRouisQuFo1CkkdpHX0XftpM9GU07LzU=
github.com/luxfi/consensus v1.36.1/go.mod h1:hmsGz3CcTrKxvw7/YSmfu8qAtzgyL5zQ3ajpUNcub/4=
github.com/luxfi/const v1.4.0/go.mod h1:qWfecjdImPY3kXsy3UAe4ZVpMcsp2/iUlHwUMPVREoI=
github.com/luxfi/constants v1.6.1 h1:4AfBh1YxDgnQjWPLqLpjiBaLAjPBw5naTTzRWWM19ms=
github.com/luxfi/constants v1.6.1/go.mod h1:Pu5jWHdnUtQRbWC43yTUjU/pbIIKMDOd2a2yroSfo48=
github.com/luxfi/container v0.0.4 h1:BXhF82WyfqVP5mjlNcr7tP0Fcnvl0Ap1rkiu+rq5XuM=
github.com/luxfi/container v0.0.4/go.mod h1:Z3SpmMF5d4t77MM0nHYXURpn+EMVaeu1fhbd/3BGaek=
github.com/luxfi/corona v0.10.3 h1:Yi1oAkW0HEsf5fvst/tUN0AjRVg6DoNHB/IC0qrFWZE=
github.com/luxfi/corona v0.10.3/go.mod h1:xe5qRir0p+FA6eETpyGDv4LjYySg1zVB13kmHpy9x94=
github.com/luxfi/crypto v1.20.0 h1:JNsQ25sVO6T8XuIHRue4akOpnt5pNmk1xg5hzmU6dNE=
github.com/luxfi/crypto v1.20.0/go.mod h1:bLCBuIV/KDjPytld7jSYe1WbfWknPQXcivq88Qo96QU=
github.com/luxfi/cache v1.3.1 h1:grQhi/B5GKypG7avDMeY143QTgFbfEvQICKNIh1Cw6U=
github.com/luxfi/cache v1.3.1/go.mod h1:2MokdbeNUy/9O3mdREWkE6BiN7tRvePkXiKkcb+4M7g=
github.com/luxfi/chains v1.7.9 h1:sDF/WQn56o/4i1/uCS8i5ENDMuyWi8+iVcnYGAstMDM=
github.com/luxfi/chains v1.7.9/go.mod h1:Qtx9JNLOWYyhnvq434YSjPINT6we9nfViUIIuGD/2bc=
github.com/luxfi/codec v1.2.1 h1:NA/O3dWm9QejQPdjLEIVx42ddVqAXsy6Y6igL/V1+aU=
github.com/luxfi/codec v1.2.1/go.mod h1:xjWOTEbw9gxY/N8nZwQPvRCfPnK/ugJHBWsb3BZ0HHs=
github.com/luxfi/compress v0.1.1 h1:cQjRYQFRrw8HinjW1T8FmKgqdRwrFYDCdecc1t0i+uQ=
github.com/luxfi/compress v0.1.1/go.mod h1:d4CkRRmwPzafIp57Jxra3RmAmNwNwU8Oc0QBBRlTqGo=
github.com/luxfi/concurrent v0.1.1 h1:LkAmNXybOsAm97nFILqMDBp/YLgleuLmyGomFT7YNxU=
github.com/luxfi/concurrent v0.1.1/go.mod h1:GlkfiJtPBpatNxvROf7hkPi4gsnmdHGmqhDnVWFrkZE=
github.com/luxfi/consensus v1.36.10 h1:LJPvHc2zL7VFhbqfYu11rA50yvxzTK17Z4mRN04HQhU=
github.com/luxfi/consensus v1.36.10/go.mod h1:iwlx11D7BRdEaUqyUvueAe2Fit1K1k26avBK2eLYdnc=
github.com/luxfi/consensus v1.36.12 h1:MJKq6j9+04Y2NC+NArGQgv5QNcjprYCk+gppCz1PdSg=
github.com/luxfi/consensus v1.36.12/go.mod h1:mCtA6k31FVW/H6rsYQ7V8VvS198WsFS8oFo/z3IMI+A=
github.com/luxfi/constants v1.6.2 h1:pXHdKIFbfE9qX4xOjq2LxYvagNhhNvspUVEbPcIEKfA=
github.com/luxfi/constants v1.6.2/go.mod h1:r0oH8C/+r/XFYBq1AJxt6zWRKKRKgDzrEMop/CCs9rI=
github.com/luxfi/container v0.2.1 h1:MTnfKXzS5+oxV5jKZerdOxSA6iMPaQI9/FWGufizzaw=
github.com/luxfi/container v0.2.1/go.mod h1:B+uM0wP0lGvt/SSK7QOEn/qBcsHzILVHlKikdCyzSgM=
github.com/luxfi/corona v0.10.4 h1:/+Uy5iOWBMkr+XACnRRiyrbb5ebsZLsUXjHfJW3sFyw=
github.com/luxfi/corona v0.10.4/go.mod h1:44Tjnm2uRG22kmmLfCzR8QEO8OXZ3jR/OnUKLsnjVJ8=
github.com/luxfi/crypto v1.20.2 h1:L81WEsU/hs2A76F5PWBusG0yU74QqkDdUqqgexWUxh4=
github.com/luxfi/crypto v1.20.2/go.mod h1:qYHOM0lO4PRh7LEaObxFQUIMjmT1/paVm/WgZkobT1k=
github.com/luxfi/crypto/ipa v1.2.4 h1:6xfwhI9/HrcDkF3Ti5/NxsNQIWbwYDJmRSNIHRQ/xfU=
github.com/luxfi/crypto/ipa v1.2.4/go.mod h1:43J6f6rcfUMrZt4cQectMOZb6Ps+fAEj8ZTPC3Kk+gE=
github.com/luxfi/database v1.20.4 h1:WOt2GIGJxf8AFpg49odMz8DZ8RFSLDrozGhZtmorN70=
github.com/luxfi/database v1.20.4/go.mod h1:S/LvmfzNYWVNslcEcZwDrntqUO2ksaL8ql1nRmLUA/Q=
github.com/luxfi/dex v1.14.0 h1:4PtorVbRmbD73S6YWPvgp5GRCb9jeuaedl7mo1CbzCI=
github.com/luxfi/dex v1.14.0/go.mod h1:wYWQmwospvdKBWQ6OJMXb8I+kfgEtE46R1rD8H7vHCQ=
github.com/luxfi/database v1.21.1 h1:GNnoWVa82l+n2dK7x2aG8LR2NEToq6ZCRX0sQjmK0OM=
github.com/luxfi/database v1.21.1/go.mod h1:Gc7Z2OPrrcYLnAL8B1trOnguXauOlSDV5tkviLN6Xec=
github.com/luxfi/dkg v0.3.5 h1:s2L2mMQaz+n9m0b0ghvoV5VZNxiwb2z4WrGugvK0udY=
github.com/luxfi/dkg v0.3.5/go.mod h1:M+WH7GFRN+YUD851Rlnumdp0Md98kplNN8pVx65U8I8=
github.com/luxfi/evm v1.104.4/go.mod h1:OjKvdO53g3LrNutLgx6N0ZXair6glO+MtueD4MHCKaE=
github.com/luxfi/fhe v1.8.2/go.mod h1:16yxwhcnCez/rNcd/C9JjH9IjbEz73X+0tvlsONyLeA=
github.com/luxfi/filesystem v0.0.1 h1:VZ6xMFKaAPBW/ddlMsDnI2G0VU1lV5rYaVcW5d+KwEY=
github.com/luxfi/filesystem v0.0.1/go.mod h1:OQVSU6XNwqrr1AI+MqkID2taHUclx7NYmmr3svgttec=
github.com/luxfi/formatting v1.0.1 h1:ZnE1rAdEUds9yAegdVdGDOBGN6hLMPOv6E03Fp8IEYo=
github.com/luxfi/formatting v1.0.1/go.mod h1:mYzNf5DJOiqSSKUPzNj5dKy4tstFbN3pZlkI5716eKc=
github.com/luxfi/genesis v1.16.1 h1:t8zFIeFg9hwl39HpYKshpB2hlHjVVBpd8va4d+FZ8T0=
github.com/luxfi/genesis v1.16.1/go.mod h1:vpnyQ/YcGINhUekrCiZWFryvP3qgYzTgFkfWoExlUdE=
github.com/luxfi/genesis/builder v0.0.0-20260607050918-bf8751181b9c/go.mod h1:BGDsFPIYLplOoBax/cpnUprd+yzLKjYtvSRt/eSP4sk=
github.com/luxfi/formatting v1.1.1 h1:MJhVXIPh1dbysvYEjtaEA/Z0FUTiI7n0DwOF54FS08c=
github.com/luxfi/formatting v1.1.1/go.mod h1:zhBWp6fLZduhpiAdPgVDdPVOyhw4FvwRUksF6+xKQCE=
github.com/luxfi/genesis v1.16.4 h1:8ZtqvgPnICgGpjBzAjYAPrI3qwr1g1DPbum+hgjc4bg=
github.com/luxfi/genesis v1.16.4/go.mod h1:0F6hV6GfwDSmIWsueB7/vqPZFxyS7A4Jo3p1QU87fFc=
github.com/luxfi/genesis/pkg/genesis/security v1.13.8 h1:9ier0p55ErSpoXHRJpZ04WV5HwwMB1uDrU7PHGBKG2U=
github.com/luxfi/genesis/pkg/genesis/security v1.13.8/go.mod h1:DzU+GYUFv12ja4Vc46bWKNBBmNYbcow3u/DASx4wpfI=
github.com/luxfi/geth v1.17.12 h1:UP/fhpcfbGPTrkOCwX3d88Oc3jVm5gTOgfjgq+lek6s=
github.com/luxfi/geth v1.17.12/go.mod h1:3vQfQJd9JC+AVBjxNXa9PYQOqpbE2dKu8E3jqhPZ3LU=
github.com/luxfi/go-bip32 v1.0.2 h1:7vFbb+Wr4Z499q2tuCLdd7wWjtn8sH+HWBlx76mhH9Y=
github.com/luxfi/go-bip32 v1.0.2/go.mod h1:bc7/LXDKAJQZ/F0Xjf5yXaTZxY9/ssLb4FC+Hxn/cDk=
github.com/luxfi/go-bip39 v1.1.2 h1:p+wLMPGs6MLQh7q0YIsmy2EhHL7LHiELEGTJko6t/Jg=
github.com/luxfi/go-bip39 v1.1.2/go.mod h1:96de9VkR2kY/ASAnhMtvt3TSh+PZkAFAngNj0GjRGDo=
github.com/luxfi/gpu v1.0.1/go.mod h1:8r1ReBPsLx1zmcA3GksKD4J7azP52FSTU/LpT9TyYHE=
github.com/luxfi/hid v0.9.3/go.mod h1:XJ/7DZAHf5dggm3zWNbitKuFGB7J96b4iX+8NO3BsnY=
github.com/luxfi/ids v1.3.1 h1:CGE3QvYzdwfDpfODAVNjMygSaueVPWXSB9yaeyCEd+k=
github.com/luxfi/ids v1.3.1/go.mod h1:6vpdcdZW0qxeade+3xby8aLTutbcJ7O0r8+fNQrksGI=
github.com/luxfi/keychain v1.0.2 h1:uQgmjs37/VBIALEiYrrszTpxvtqr07/YvS9TnmxGafs=
github.com/luxfi/keychain v1.0.2/go.mod h1:q/4ULgZBlstKkwzOzG/0T6y73BDPgnkrcibbJyTvmbU=
github.com/luxfi/keys v1.2.0 h1:3TAcr4twyMpwQp7J29ZRtIa5vzAoDrnXnLcPKVHJWmw=
github.com/luxfi/keys v1.2.0/go.mod h1:SjsAaxo6sGmSp9OaHXUiVCqsknO8iPspN6jMOoEAMb8=
github.com/luxfi/kms v1.11.7 h1:E25z8SCNTGOVvzzg5tj6pwJQ2K3FrE/nuy0KAfF+0zs=
github.com/luxfi/kms v1.11.7/go.mod h1:XhLUVqN4RBv6j4Bj3MNgTZmHCnm74jH7RqqK0b9xbzw=
github.com/luxfi/geth v1.20.1 h1:QUGQr4AKvADjwMi7t8a0OfoyxShgEcI9pwie1jFYfm0=
github.com/luxfi/geth v1.20.1/go.mod h1:GV5bIMEgWviRN+jPXERyVpI16H3iHqPcdIokDoZdrvU=
github.com/luxfi/go-bip32 v1.1.0 h1:zjy19WKa1KJnGamRNyOZM3l/MPtV/sax7M4NMPwLHZQ=
github.com/luxfi/go-bip32 v1.1.0/go.mod h1:QyDXlzWL3xRiCbMUi4Z3J52Q/SMoCvdRLXXlYvSZor8=
github.com/luxfi/go-bip39 v1.2.0 h1:fx3pFuSGawCG4In6pA4OLLStqbgIqD1j8EygFskoHzY=
github.com/luxfi/go-bip39 v1.2.0/go.mod h1:if+2OVbG4k4jKIuBt/Rse1KV1kgWQM5j5xFbUtwbNtc=
github.com/luxfi/ids v1.3.2 h1:c6Rft5kZB4XqiCtWaGH47bfhaNFm3FGRfhEzI01GVeI=
github.com/luxfi/ids v1.3.2/go.mod h1:+5l8cYMbKpORJbQ2r98CYJo9TQATgUdnmzpYFZWMwwc=
github.com/luxfi/keychain v1.1.1 h1:dTYEPy6CGVC1sogMci4iJogUvW6VdTmemplQdzRqnAs=
github.com/luxfi/keychain v1.1.1/go.mod h1:hAzBcwxGumtoYrM5hfhwdt8wE0p7r2JCd5AxswqfkoY=
github.com/luxfi/keys v1.4.1 h1:2Zcoovaz9OLPz7m7VGXfRrGnrlqt0GeUpJclsPBi4EU=
github.com/luxfi/keys v1.4.1/go.mod h1:P8EUP5DKrR1SUZBGZjDT3rWcp2P1miUlVh7IBRNBphU=
github.com/luxfi/kms v1.12.10 h1:fXgisnBkixFSrqhOFbZVRQkf8cX4q4vS3Gix1qjL3PQ=
github.com/luxfi/kms v1.12.10/go.mod h1:CCcWDXIlDT2TwfYAUxedLyzShFRJAlVlp15zQ2L3CrU=
github.com/luxfi/lattice/v7 v7.1.4 h1:hQR02M6cHTAV5+joOPi9gb9Gm+z/hKJnhJF4IlciIJs=
github.com/luxfi/lattice/v7 v7.1.4/go.mod h1:DmIQFi3mJiehVsR235l1NKYEU0JhU649OX5p7gMEW2c=
github.com/luxfi/ledger v1.1.6/go.mod h1:hyNV+4a6nI3yfwhyJcmXICwJeWfnxA1PbMZEOZ1VFnw=
github.com/luxfi/lens v0.1.4 h1:goGjGDXx2BNdjzXDunL5QT8elK2ZyCcc0z8TAbtWYrg=
github.com/luxfi/lens v0.1.4/go.mod h1:mL+G8IK+9L41d78/2FYRgfhEzAjcr5+VEXB8SGuHbus=
github.com/luxfi/lens v0.2.1 h1:5Qd0GdjbM+XUVgwDbZ452tKkR7yeE8QnBTHHaH8fJNY=
github.com/luxfi/lens v0.2.1/go.mod h1:6FIhC8weEE5RbNMF3SaE+XPSB9cr6FmjypYBoHkz4JQ=
github.com/luxfi/light v1.0.0 h1:zTJgp5M0xX8rIwRjYDQgOGhLas3rgXyhkszrTX+ne3s=
github.com/luxfi/light v1.0.0/go.mod h1:1G0kgjEe/srlBMCIrFq4IvhnrMuHKFG18CRUWfw1z30=
github.com/luxfi/log v1.4.3 h1:xkUKRWvQ4ZwvlUC2e0/RTtHYZOYSMvSQ9W9lbjwBmiI=
github.com/luxfi/log v1.4.3/go.mod h1:myIkufyiQomSQH34K981kbz6cG4WUoerRUh7F4XhlQI=
github.com/luxfi/lpm v1.9.4/go.mod h1:K9NTVtLpFZ/ojG4fs963108I8My58gyO6DlnULoxag0=
github.com/luxfi/magnetar v1.2.3 h1:n4UrJZLK+mhDDZr1HLl2H/KgA6o6v62r5oiC61R7awE=
github.com/luxfi/magnetar v1.2.3/go.mod h1:z9PLkqzzYiaFGT/qFBQSnNoHmZrg8y7JlYGiNnHAAdk=
github.com/luxfi/math v1.4.1 h1:1t9bCCsEqnl9yIKrShlbs80DBKyYTWdnzkVfBqEeO7Q=
github.com/luxfi/math v1.4.1/go.mod h1:QvbRxauQyE1w4lvbcLSe6c8yeJz2Zj1Bq1rayGgs2tA=
github.com/luxfi/math v1.5.1 h1:FDOY75e4vn/Xra1ij99xOS/9XdxQGCPP6HONHRkCwfg=
github.com/luxfi/math v1.5.1/go.mod h1:3j9R24hVfPhrbvs45YSJP7jAyVNfwx/cj/+lAO8IGro=
github.com/luxfi/math/big v0.1.0 h1:Vz4c0RsZVPdIKPsHPgAJChH/R3p15WHRUz7LkLf+NIQ=
github.com/luxfi/math/big v0.1.0/go.mod h1:BuxSu22RbO93xBLk5Eam5nldFponoJ73xDFz4uJ3Huk=
github.com/luxfi/math/safe v0.0.1 h1:GfSBINV9mOFgHzd32JbgfHSLhlNn0BwnP43rteYEosc=
github.com/luxfi/math/safe v0.0.1/go.mod h1:EejrmOJHh03YAD8+Zww8cPcMR1K3Q2I7w1dX4sMloeo=
github.com/luxfi/mdns v0.1.1 h1:g2eRr9AXcziPkkcd24M+Qu9ApEpoKKjfI79QSNqv0rQ=
github.com/luxfi/mdns v0.1.1/go.mod h1:dbp5f3h3aE7CGzwbaWzBM9cwdcekhmSrWhQevgYhhNA=
github.com/luxfi/metric v1.6.0 h1:PIxHOk8R0qs5etnWsUSPoZ5wGh37APgUiSt4gijOl80=
github.com/luxfi/metric v1.6.0/go.mod h1:ux+w3RZQCfF1zM8MO0wAWyNj/CsDlPd2mwTGshB9vY0=
github.com/luxfi/mlwe v0.2.1 h1:pRwTjNUUtzUxRIlMbUPpeh9DE2/NdqfS17hfdogazp4=
github.com/luxfi/mlwe v0.2.1/go.mod h1:DD9EHTeiyh/y0KGGeqL+q9S4n8raeGiGdaG/BQPAvT0=
github.com/luxfi/metric v1.8.1 h1:v58GgPFAOLPVxSa/JiNLwqJQNEFHdWbXZV28piMXX4s=
github.com/luxfi/metric v1.8.1/go.mod h1:R1OPAIeW4UBW3osK7j2r3/XPmczfNRFTXg4bnlemTuE=
github.com/luxfi/mlwe v0.3.0 h1:5mtXLbO2RxaE45r75sj43c6UdpjDKQ5nTQcOGuoRQT8=
github.com/luxfi/mlwe v0.3.0/go.mod h1:DD9EHTeiyh/y0KGGeqL+q9S4n8raeGiGdaG/BQPAvT0=
github.com/luxfi/mock v0.1.1 h1:0HEtIjg1J6CWz+IUyP6rsGqNWTcmxjFnSQIhaDuARwY=
github.com/luxfi/mock v0.1.1/go.mod h1:jo35akl3Vtd8LbzDts8VJ0jmSVycrd1/eBi6g6t5hKU=
github.com/luxfi/net v0.0.5 h1:F1lD3NsIioV0wr2V5jWc4TtMyiE/Ffo1LoeblFv3TrI=
github.com/luxfi/net v0.0.5/go.mod h1:BEQR1HEVmkjii/F1R6vJrNUVE7wr55b4eMq9Iz5wjUw=
github.com/luxfi/netrunner v1.19.2/go.mod h1:e9oiUxS1B49UNbps275fHrKOnv98/dQi67bIkjM1NhI=
github.com/luxfi/oracle v1.0.0/go.mod h1:7xmiYMZwKiIazVr56lMHDxC0kMF0/Y/9OLSdOBBQ7/M=
github.com/luxfi/ordering v0.0.1/go.mod h1:Ld5UAayScKxicsd9BYuQ/D7vzEUndOC7cGCT+fj++jM=
github.com/luxfi/p2p v1.21.1 h1:gmz1JMDhzHIL3dQlhwIDvR4OlFuhNVfnWUl/ipYhAIo=
github.com/luxfi/p2p v1.21.1/go.mod h1:SsNPR5fPGWWNem9plGWhSmRqyDoysJ3kPAN0zG0g3iw=
github.com/luxfi/password v0.0.1/go.mod h1:xKhi28+V3xqPc4+kIZHRp7oZ1W7nUKze6EauvMVq19s=
github.com/luxfi/net v0.1.1 h1:jIQCb8ulBGEvHIcorzDDNCeWzutFzLVtRWodutrQE24=
github.com/luxfi/net v0.1.1/go.mod h1:SwxbUQ538u4QAcgC/N61uahDk1TDHL6Ku89CX/WV2lk=
github.com/luxfi/p2p v1.22.1 h1:M59Iy+FIJta99aFfTpG6EE70jm9uqIX+iHrGBYPHDhg=
github.com/luxfi/p2p v1.22.1/go.mod h1:FHOSavVcq8JS1ZQtfddd9Jj1gaPstz1cjvNgczAQRyg=
github.com/luxfi/pq v1.1.0 h1:ADplfUSyirLymSxs3Ix0HeDTyl5oswCNUpXJt/5vLY8=
github.com/luxfi/pq v1.1.0/go.mod h1:KT5rG9ztpzIkT9QSnXK4WFqBBLzKCLjY7l1c/unBi8I=
github.com/luxfi/precompile v0.19.1 h1:nTfhwrubwQKED5SAOFqIbFO1o6J49IZNdSYbOEaiPpA=
github.com/luxfi/precompile v0.19.1/go.mod h1:AOMGWGFXHtnGVYjel/mP/7Dt60e2u7ef0SswY4j8F+k=
github.com/luxfi/proto v1.3.5 h1:AW11rnu5xyvB7beyowoiY9uIffLOF3+eMR/a3EkK2c8=
github.com/luxfi/proto v1.3.5/go.mod h1:ixTofGpdW1rTYr+wgTuBhAsgBv8GnWYHMLWbPNEdm7M=
github.com/luxfi/pulsar v1.9.0 h1:c0JnatYF79aN87aof9VlYjIoCzmixxrgNPeUUuh8ScU=
github.com/luxfi/pulsar v1.9.0/go.mod h1:1+/atAiiiOm9RnXM3c66eHF3garjAa3C+sn4rAU7JUU=
github.com/luxfi/relay v1.0.0/go.mod h1:srhFAQ3dS+WBTRwPqzinZiw+QbWdRhajT+W7k9ShjrE=
github.com/luxfi/resource v0.0.1 h1:mTh+ICWSy548GTUSSyx7V/X5dV18oEwxZeQEYGJQhD4=
github.com/luxfi/resource v0.0.1/go.mod h1:wWpZktciYwIi6RNqA+fHwzmPrUJa7PRX7urfwT+spRE=
github.com/luxfi/precompile v0.19.3 h1:ljJholS+9mlR8EjbfM96BLYvIni7xCt7KkX06YzAIJU=
github.com/luxfi/precompile v0.19.3/go.mod h1:c+dh+FWfpKr3FNfI4LhIdnn0naQmmS5Yz0KKTt4LknY=
github.com/luxfi/proto v1.4.2 h1:dEGTE7xOWVPTXRZNDBJfwh2Q19vE7MQBhyXXmWenw+8=
github.com/luxfi/proto v1.4.2/go.mod h1:vwVkrC9ghhuCL8bluA59+G5jaD6WuqiDyd1iKh7yzOE=
github.com/luxfi/protocol v0.0.2 h1:TAuXMm1K4VDpG3B3brq1EE9Lb7XlbhQmVBObskgNhmc=
github.com/luxfi/protocol v0.0.2/go.mod h1:Yjn2VRwn5PXim0+JTalAyrVG6YbmuSnYOfeUXSbAsqA=
github.com/luxfi/pulsar v1.9.2 h1:pFLoAfBlCwFZfchqHn78J6yMe29AhaoKGRa/KTUP8CE=
github.com/luxfi/pulsar v1.9.2/go.mod h1:uTOtribcUvTTwAOy0Ztg0S2AUiNAsVqFfopTrKW+zjM=
github.com/luxfi/resource v0.1.1 h1:k11s5xLGX85UWq/iLZyWLhnqeLTlp3FHEt8u/8AHdkY=
github.com/luxfi/resource v0.1.1/go.mod h1:s7SbZOSVbgj9bWFOcLCcXgnMlHxbqtqhAeJ3f0Xuy/Y=
github.com/luxfi/rpc v1.1.0 h1:B/PJbK399th1mHRDSufhCpVbAciZqId3LsaWhIGNWH4=
github.com/luxfi/rpc v1.1.0/go.mod h1:s0bI7/Wg1ZdFdG/cQK+4pZNdEmUsXNBA3HeZRZ+XLeM=
github.com/luxfi/runtime v1.1.3 h1:6Yp/PKwQCohjXmBR9GA+gamdSAp+xA2rdN6J/74Y4aw=
github.com/luxfi/runtime v1.1.3/go.mod h1:r1uonDnxRCnPz6N6WYwaC72HW95KbFIAyChnJyxePGs=
github.com/luxfi/runtime v1.3.1 h1:vsQZ3sl6XMeyHuNGCMM86d0whrE/lhMre4CCJaClPdE=
github.com/luxfi/runtime v1.3.1/go.mod h1:Tct998uUcmCQbUC1WLEeGLDH2IaVgMb+SkKcHKZpHV0=
github.com/luxfi/sampler v1.1.0 h1:u3iRDl7V06ARh0e85h3HT+aZ1saCFo2yMMsh+dCJbqk=
github.com/luxfi/sampler v1.1.0/go.mod h1:kJa53S3tC9+VSbuV3RFu68MmbCCBlr2UM39LOClQ/Hs=
github.com/luxfi/sdk v1.17.9 h1:MfExzWNym7IicO2egiHg6N0WnImLtAUpjCpiD/zc2ZE=
github.com/luxfi/sdk v1.17.9/go.mod h1:XvZuopyltjR4SvHvA1c6wtNcnO+FzLyjfm0v+FyN9sI=
github.com/luxfi/staking v1.5.1 h1:f9MaGnRm0xc02crDm5Qs1T2r88d3KzNkHZypAvsmAlU=
github.com/luxfi/staking v1.5.1/go.mod h1:lT7KLaiTpdq3lg78H0gp2qSEfX9LaK1vs7w73XV/9nw=
github.com/luxfi/sdk v1.18.1 h1:KnqkL6bNATtQXHbnTMsue5pLz9tn74YFAoux2ek+k90=
github.com/luxfi/sdk v1.18.1/go.mod h1:8BE1iF2ewkDbsV31TPE0/HT7RGJILR1ZALbKE1GgHMc=
github.com/luxfi/staking v1.6.1 h1:be023mY88AnFgF9P+MMvILX21I2CaqVAVkGcc+a20so=
github.com/luxfi/staking v1.6.1/go.mod h1:X8i/uCLc009mlIUnFMErrQMCwfGaOVCAVf+GbDN5nn4=
github.com/luxfi/sys v0.1.0 h1:M7RYOt8W4Wws7cxxsyOHe50UKMYTzIu7HYknqW4xt0Y=
github.com/luxfi/sys v0.1.0/go.mod h1:GT8vGdYTfoqRy9/11blmRuqPPypzwrudCTHZXT+ru9M=
github.com/luxfi/threshold v1.12.1 h1:pA6ZB8Qv6BStprSemfoCY3fD7P5PEod36Nj6FmJR1jQ=
github.com/luxfi/threshold v1.12.1/go.mod h1:iuRQGDAy8ZKjQhZjkSKg7NtbP75/8Up9zj52y7IuyZo=
github.com/luxfi/timer v1.0.2 h1:g/odi0VQJIsrzdklJUG1thHZ/sGNnbIiVGcU6LctJm0=
github.com/luxfi/timer v1.0.2/go.mod h1:SoaZwntYigUE3H6z1GV32YwP8QaSiAT0UiEv7iPugXg=
github.com/luxfi/tls v1.0.3 h1:rK3nxSAxrUOOSHOZnKChwV4f6UJ+cfOl8KWJXAQx/SI=
github.com/luxfi/tls v1.0.3/go.mod h1:dQqSiGE7YxXUxOwICoReUuIitBms9DYOaCeteBwmIWw=
github.com/luxfi/trace v1.1.0 h1:eQoObwStrdQN879zfJWJeN1l0FJnfOKQHQHyEJOYjCI=
github.com/luxfi/trace v1.1.0/go.mod h1:Sgtpj8ZE5GBSi4ZyQOL3rL9enl59sSWswWWKw3BUdpk=
github.com/luxfi/threshold v1.12.3 h1:CYqcPfzVUs0M/+e/ja1q7aLBZZUi6Rv/XIXR5lrZ7aE=
github.com/luxfi/threshold v1.12.3/go.mod h1:xQT8xzmh9pQ1CdBgpl7P3ezXQZcqTPv3tqpIytvuiTA=
github.com/luxfi/timer v1.1.1 h1:54GiNBKydQ0VF5/EwVc/mCsbqe0yJNfZV7Ae8qJhCwk=
github.com/luxfi/timer v1.1.1/go.mod h1:OXY/8ZFKCdEsimpfnUG1MQWvzjjFbsmBOiW/m6KSrC0=
github.com/luxfi/tls v1.1.1 h1:BSZ0gHSp7U8vzlmzx7WSSCz+b7Ky4JtD9HDDhn7vrDg=
github.com/luxfi/tls v1.1.1/go.mod h1:+5TDy8UtLL+tz124brZzpUDBRj+sKrq0JFqdmpMUHgw=
github.com/luxfi/trace v1.2.1 h1:MPV079P2eTijB7F06AyJU1HJwpQVxRx1qYXhva9YsvM=
github.com/luxfi/trace v1.2.1/go.mod h1:/bX8g0RRHPHUq7kX8of/Aaq6C7rD0JuNH57vVbw7ZG8=
github.com/luxfi/units v1.0.0 h1:2aNVB+WsP1XeDob71IsO0w3jJqP3FtZdYnFsmORkJZg=
github.com/luxfi/units v1.0.0/go.mod h1:tma28v4ed1tupdS0kpSeyO+u1wWK/g1NqODPbN1YzmA=
github.com/luxfi/upgrade v1.0.1 h1:7+ygYeUf/MuLeGL7pjIu6ckQimxctCp+Swybhpy64go=
github.com/luxfi/upgrade v1.0.1/go.mod h1:Re7g9Y+SYf/LvkHFpN0vbtlVH/Rr5ZpHQdPeVFEo3Jw=
github.com/luxfi/utils v1.2.0 h1:gtEiI7/NM6PQ/OasEpH0PvB+e5hIS/tpum9r64pYjMc=
github.com/luxfi/utils v1.2.0/go.mod h1:T2OCKT1xG9jtKR/gyJQoSkticzrE9WFQ8eohJHGu9Fg=
github.com/luxfi/utxo v0.5.3 h1:3RJ6Ow5zYs2atd6QkhRNsW/cgmrQKz/UOFI25ULebQ8=
github.com/luxfi/utxo v0.5.3/go.mod h1:YF2r3EGm9pGviZuFBY5tX6rIIYmF+2KvFC/OkW/gqTg=
github.com/luxfi/utxo v0.5.5 h1:57iLgiP2nY6d7ljvDpSoQefNbf9+S4Wknj0k99eQ20g=
github.com/luxfi/utxo v0.5.5/go.mod h1:Qom/3mk7+9aEziXzdEGvHxTAl7lA2IsSDs+WUkFxjRY=
github.com/luxfi/utxo v0.5.6 h1:xdmHmgzhq71rVLd752g4gzl7CFeUsKpxttDdzTQGXKc=
github.com/luxfi/utxo v0.5.6/go.mod h1:Qom/3mk7+9aEziXzdEGvHxTAl7lA2IsSDs+WUkFxjRY=
github.com/luxfi/utxo v0.5.7 h1:ocmCvtL5/QrxcDBjwISD3gKKyydeNpnlFHZ4E+/YYIM=
github.com/luxfi/utxo v0.5.7/go.mod h1:n5Rzk6idEPAdTFLnioVQDhw+ypVtyE2TiJqWaTX7ZIA=
github.com/luxfi/validators v1.2.0 h1:VygpiBqBAdGrfkb7xzE2yrVmnXaqE+hm8FLWdGXO7G8=
github.com/luxfi/validators v1.2.0/go.mod h1:GYLulrNXAan23ZlX7sgWVbVnLpUexeB/m2qr2ymsXok=
github.com/luxfi/upgrade v1.0.3 h1:mFMfIb88HzoL4fp7I6w1g+VnxlAWj6UQdZOdf4AkCLk=
github.com/luxfi/upgrade v1.0.3/go.mod h1:3c/u4T/n7EsODofkWg5VigSt9BATQM8EmaNttfHMCrU=
github.com/luxfi/utils v1.3.1 h1:Us02ag60kGu94B41XIelExa7c+K6zPKwDJyq+eB+hc0=
github.com/luxfi/utils v1.3.1/go.mod h1:ROZrzpt6Kx8ttS1mo12oqsOzRB088GQ1h9jXEoDDpNA=
github.com/luxfi/utxo v0.5.8 h1:HydTOKERb8vY0Z39Dvg/V3qg7My3N/eXOY4nLv3iezg=
github.com/luxfi/utxo v0.5.8/go.mod h1:kqkwMm99NbWwGZrOzuRzF0vck+lCJwrlejmmCwj5pZc=
github.com/luxfi/validators v1.3.1 h1:+/7j0CTXlMKyaSLFM+gd6Fq64/edORJfrD/xGnf7Xcg=
github.com/luxfi/validators v1.3.1/go.mod h1:sIQyUZOvXoJ/9/RCOhHSzjumZIoxiqacNOn5mQWVozU=
github.com/luxfi/version v1.0.1 h1:T/1KYWEMmsrNQk7pN7PFPAwh/7XbeX7cFAKLBqI37Sk=
github.com/luxfi/version v1.0.1/go.mod h1:Y5fPkQ2DB0XRBCxgSPXp4ISzL1/jptKnmFknShRJCyg=
github.com/luxfi/vm v1.2.7 h1:/lHRgSU/Jmn3D5hBg4R3ZntWnY/L5fjF8JcYPoqcIjc=
github.com/luxfi/vm v1.2.7/go.mod h1:o52+zrBZCqBPrAO0dIAmK5Px7oKevT0sup5LssgFdYM=
github.com/luxfi/warp v1.24.0 h1:jrcJNlbOiZsAEopJMy9bSaCwI5NDZ8qgp/6sNoXqepg=
github.com/luxfi/warp v1.24.0/go.mod h1:bKvTi24JHlANsl7qkWZAVr/DsMfvwy42f+Cc9x4+Sq8=
github.com/luxfi/zap v1.2.2 h1:1WoijKzhx7P//fExdv9P9GNJS1rtpwlBzHLDh5TBc30=
github.com/luxfi/zap v1.2.2/go.mod h1:JfqII8VtVQYLLTX6obU1DP9sjGqf9L24vfug5ifh0b8=
github.com/luxfi/zap v1.2.3 h1:aLPUgXH5ITqbGRS6tFx1hMZqsB4kL6j39+4s7/JlGgs=
github.com/luxfi/zap v1.2.3/go.mod h1:JfqII8VtVQYLLTX6obU1DP9sjGqf9L24vfug5ifh0b8=
github.com/luxfi/zap v1.2.4 h1:1hqvo+vKZ+umYYJnxqYvVsZ+D7Os1BDmrLlqISSk/dA=
github.com/luxfi/zap v1.2.4/go.mod h1:JfqII8VtVQYLLTX6obU1DP9sjGqf9L24vfug5ifh0b8=
github.com/luxfi/zap v1.2.5 h1:SfUzw4cp01xGfDoKRQBRzXsxCqY7jQ2UFJo/KWkhu8I=
github.com/luxfi/zap v1.2.5/go.mod h1:JfqII8VtVQYLLTX6obU1DP9sjGqf9L24vfug5ifh0b8=
github.com/luxfi/zapcodec v1.0.1 h1:pRxLxCOi6uihQMg8A8riDjNjefU2cXZxfRVZ+obeuL8=
github.com/luxfi/zapcodec v1.0.1/go.mod h1:txrRt2JK4O76ssTxlXIwoNVsgzyZVL0ES4mlXqGNogs=
github.com/luxfi/vm v1.3.1 h1:U2Bv7IDRFv8JtjUARfY53ZnOPRB2iPrPHSMDCQ+T0Js=
github.com/luxfi/vm v1.3.1/go.mod h1:6YR/uFV2FoofmogQShv+HM7V8alz5rbrQYjp5w0bNVA=
github.com/luxfi/vm v1.3.3 h1:BEBamD9VUcPG8mtL2Ga0Us1lTQ+ubaZqng/PZjNqzPc=
github.com/luxfi/vm v1.3.3/go.mod h1:v2XjW0yymxHwy1xPsSz338s0J8nRqOIb6RiDf0wUEgk=
github.com/luxfi/warp v1.24.1 h1:9F+z6fy4sxmXp+3LlR9m3TiZ8Dfthh+s5KOeewSJL30=
github.com/luxfi/warp v1.24.1/go.mod h1:kRGQDt6EB3oRLYo71aXqnmJuCBVfND3oQ5/2miaLoyg=
github.com/luxfi/zap v1.2.6 h1:NBpbm9Gib41Oi/XAkAZKQ3hb+xCafo7JsrUjw+bKiAc=
github.com/luxfi/zap v1.2.6/go.mod h1:sTAe/AMMamoE85cVoe81+NbqHJkgvqS0LhY9ByHEmr0=
github.com/luxfi/zapcodec v1.1.1 h1:SdYexj7oWdks/wfF9s+8m/9PAYt3S8QjORxURutvIs0=
github.com/luxfi/zapcodec v1.1.1/go.mod h1:fmmgd8C/JrQdh3b1OzBkrvPN4TYs5uRfVV3sVtbiLbQ=
github.com/luxfi/zapdb v1.10.1 h1:XV3k4UTTKKxUMgbfC7woPXgUEIJd3P5nj2lGTQ88xeE=
github.com/luxfi/zapdb v1.10.1/go.mod h1:3Y0hH2A9kvjR+Bp9N2yEbtHnhXGHhqCQOLvBRkHrrM0=
github.com/luxfi/zwing v0.5.2 h1:2+nDKHVIdT8GvaKO79GuO3x/3DgJvX0gI32h4P+P6RU=
github.com/luxfi/zwing v0.5.2/go.mod h1:8nixkEL3bhO2LrqVqhJ8WgT+QGUtnCPIQIhFQh9gQio=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/luxfi/zwing v0.6.1 h1:Ve7RvYVBXx4JWe8TZhLfpez1N1G685hl/PSMDsR4RI4=
github.com/luxfi/zwing v0.6.1/go.mod h1:Eal4hnjmdFXc6rxciA6cxeCfV1BKs8le03v83W5oiKg=
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/melbahja/goph v1.4.0/go.mod h1:uG+VfK2Dlhk+O32zFrRlc3kYKTlV6+BtvPWd/kK7U68=
github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE=
github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
@@ -572,36 +469,23 @@ github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.100/go.mod h1:EtGNKtlX20iL2yaYnxEigaIvj0G0GwSDnifnG8ClIdw=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/pointerstructure v1.2.1 h1:ZhBBeX8tSlRpu/FFhXH4RC4OJzFlqsQhoHZAz4x7TIw=
github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.9.0 h1:tsBJ0RXwph9BmAuFoCmqGv6e8xa0MENQ8m0ptKq29mQ=
github.com/montanaflynn/stats v0.9.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI=
github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6FxaNu/BnU2OAaLF86eTVhP2hjTB6iMvItA=
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oapi-codegen/runtime v1.3.1/go.mod h1:kOdeacKy7t40Rclb1je37ZLFboFxh+YLy0zaPCMibPY=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
@@ -622,7 +506,6 @@ github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 h1:+FZIDR/D97YOPik4N4lPDaUcLDF/EQPogxtlHB2ZZRM=
@@ -639,11 +522,9 @@ github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkY
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
@@ -652,20 +533,15 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/common v0.68.0 h1:8rQJvQmYltsR2L7h8Zw0Iyj8WYNNmpwikoQTZXwfVeA=
github.com/prometheus/common v0.68.0/go.mod h1:4soH+U8yJSROk7OJ//hmTiWKsxapv6zRGgTt3keN8gQ=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/protolambda/bls12-381-util v0.1.0/go.mod h1:cdkysJTRpeFeuUVx/TXGDQNMTiRAalk1vQw3TYTHcE4=
github.com/protolambda/zrnt v0.34.1/go.mod h1:A0fezkp9Tt3GBLATSPIbuY4ywYESyAuc/FFmPKg8Lqs=
github.com/protolambda/ztyp v0.2.2/go.mod h1:9bYgKGqg3wJqT9ac1gI2hnVb0STQq7p/1lapqrqY1dU=
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
@@ -676,15 +552,8 @@ github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88ee
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
@@ -696,7 +565,6 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/status-im/keycard-go v0.3.3/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -731,34 +599,33 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.73.0 h1:ocTOORnBWtJ+P8t/6wAjdkchMzdfHmWx2VD/DPbgZ7s=
github.com/valyala/fasthttp v1.73.0/go.mod h1:EtXQDHaR+5P18p8wqDRFpUhxr108Ga9mXvVJXHRrN2k=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zap-proto/http v0.0.0-20260506200741-fd6047874433 h1:7WsCr/pZvWozimdYNffL3B9K6gLr8w0Z7WAi1+eZWtc=
github.com/zap-proto/http v0.0.0-20260506200741-fd6047874433/go.mod h1:xySyVTIwjknmVE+p+6rukkX86rFZtXeAu/QY7KXMYqw=
github.com/zap-proto/go v1.1.0 h1:DDGhuTBNqkmNax7QV/VvkInJR7hiOcybwUjN3YBt8fg=
github.com/zap-proto/go v1.1.0/go.mod h1:914SNGTH6Rv3Yu1MweWJBPEN8FZlo5C39QyhaB0C7Q0=
github.com/zap-proto/http v0.3.0 h1:l7DvlngiYqmzNY6fzyRYw2ZIAhF35FqwOe6mvAOqpMg=
github.com/zap-proto/http v0.3.0/go.mod h1:UYfGhDDCetgxs65XSev8Lpf65COg5vKQK+cWwZGh4zQ=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
@@ -769,12 +636,8 @@ go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/zpages v0.62.0/go.mod h1:C8kXoiC1Ytvereztus2R+kqdSa6W/MZ8FfS8Zwj+LiM=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
@@ -783,10 +646,8 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfC
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
@@ -803,17 +664,16 @@ golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9 h1:4d4PbuBNwaxMXkXI8yiIYjydtMU+04RHeuSxJdgKftM=
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -828,17 +688,15 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/perf v0.0.0-20230113213139-801c7ef9e5c5/go.mod h1:UBKtEnL8aqnd+0JHqZ+2qoMDwtuy6cYhhKNoHLBiTQc=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -861,19 +719,17 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -883,10 +739,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -894,10 +748,6 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
gonum.org/v1/plot v0.15.2/go.mod h1:DX+x+DWso3LTha+AdkJEv5Txvi+Tql3KAGkehP0/Ubg=
gonum.org/v1/tools v0.0.0-20200318103217-c168b003ce8c/go.mod h1:fy6Otjqbk477ELp8IXTpw1cObQtLbRCBVonY+bTTfcM=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
@@ -915,15 +765,11 @@ google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -933,19 +779,3 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug=
k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
+12
View File
@@ -34,6 +34,11 @@ type Net interface {
// IsBootstrapped returns true if the chains in this chain are done bootstrapping
IsBootstrapped() bool
// Bootstrapping returns the IDs of the chains in this net that have NOT yet
// finished initial sync. The net owns the bootstrapping set, so it is the
// net — not its caller — that can name those chains.
Bootstrapping() []ids.ID
// IsChainBootstrapped reports whether a SPECIFIC chain in this net has finished
// initial sync — i.e. Bootstrapped(chainID) was called for it (the chain reached
// the network frontier and its VM went to normal operation). This is the per-chain
@@ -75,6 +80,13 @@ func (s *chain) IsBootstrapped() bool {
return s.bootstrapping.Len() == 0
}
func (s *chain) Bootstrapping() []ids.ID {
s.lock.RLock()
defer s.lock.RUnlock()
return s.bootstrapping.List()
}
// IsChainBootstrapped assumes MONOTONIC per-process bootstrapped state: a chain
// only ever moves bootstrapping→bootstrapped (Bootstrapped is forward-only and
// AddChain refuses to re-add a chain already in either set), so this signal — and
+23
View File
@@ -68,3 +68,26 @@ func TestIsAllowed(t *testing.T) {
require.False(s.IsAllowed(ids.GenerateTestNodeID(), false), "Non-validator should not be allowed with validator only rules and allowed nodes")
require.True(s.IsAllowed(allowedNodeID, true), "Non-validator allowed node should be allowed with validator only rules and allowed nodes")
}
// Bootstrapping must name the CHAINS still syncing. The net-level aggregate
// IsBootstrapped() only says "something here is unconverged"; only the chain
// IDs say what.
func TestNetBootstrappingNamesTheChains(t *testing.T) {
require := require.New(t)
chainID0 := ids.GenerateTestID()
chainID1 := ids.GenerateTestID()
s := New(ids.GenerateTestNodeID(), Config{})
require.Empty(s.Bootstrapping())
s.AddChain(chainID0)
s.AddChain(chainID1)
require.ElementsMatch([]ids.ID{chainID0, chainID1}, s.Bootstrapping())
s.Bootstrapped(chainID0)
require.Equal([]ids.ID{chainID1}, s.Bootstrapping())
s.Bootstrapped(chainID1)
require.Empty(s.Bootstrapping())
}
+74 -5
View File
@@ -14,9 +14,10 @@ import (
"github.com/luxfi/math/set"
"github.com/luxfi/node/message"
"github.com/luxfi/node/proto/p2p"
"github.com/luxfi/node/trace"
"github.com/luxfi/node/version"
"github.com/luxfi/timer"
"github.com/luxfi/node/trace"
luxversion "github.com/luxfi/version"
)
// router implements Router interface for routing messages to chain handlers
@@ -193,26 +194,94 @@ func (r *chainRouter) AddChain(ctx context.Context, chainID ids.ID, h handler.Ha
)
}
// versionedConnector is the capability a chain handler advertises when it can
// forward a peer's REAL application version to its VM. The consensus
// handler.Handler.Connected signature carries only the nodeID (the version was
// dropped at that boundary); a handler that implements this receives the real
// version instead. blockHandler implements it. The version must survive to the
// inner VM: the C-Chain (coreth) state-sync peer tracker compares peer versions
// and dereferences a nil version, panicking a state-syncing node.
type versionedConnector interface {
ConnectedWithVersion(ctx context.Context, nodeID ids.NodeID, nodeVersion *luxversion.Application) error
}
// toAppVersion converts the node's peer version (github.com/luxfi/node/version)
// to the github.com/luxfi/version.Application the VM Connected boundary
// (chain.VersionInfo) expects. nil-safe: a nil peer version maps to nil.
func toAppVersion(v *version.Application) *luxversion.Application {
if v == nil {
return nil
}
return &luxversion.Application{
Name: v.Name,
Major: v.Major,
Minor: v.Minor,
Patch: v.Patch,
}
}
func (r *chainRouter) Connected(nodeID ids.NodeID, nodeVersion *version.Application, netID ids.ID) {
r.lock.Lock()
defer r.lock.Unlock()
r.connectedPeers.Add(nodeID)
handlers := make([]handler.Handler, 0, len(r.chains))
for _, h := range r.chains {
handlers = append(handlers, h)
}
r.lock.Unlock()
r.log.Debug("peer connected",
log.Stringer("nodeID", nodeID),
log.Any("version", nodeVersion),
log.Stringer("netID", netID),
)
// Deliver the connection to every chain handler so VMs observe peer
// connectivity — the P-chain uptime tracker in particular. Handlers dedup, so
// repeated dispatch of the same connection (once per tracked network) is
// safe. Dispatch OUTSIDE the router lock: a handler must never re-enter the
// router while we hold it.
//
// Deliver the REAL peer version through the versionedConnector capability so
// it reaches the inner VM (proposervm → coreth state-sync). Only handlers
// that cannot carry a version fall back to the plain nodeID-only Connected.
appVersion := toAppVersion(nodeVersion)
for _, h := range handlers {
var err error
if vc, ok := h.(versionedConnector); ok {
err = vc.ConnectedWithVersion(context.Background(), nodeID, appVersion)
} else {
err = h.Connected(context.Background(), nodeID)
}
if err != nil {
r.log.Debug("chain handler Connected failed",
log.Stringer("nodeID", nodeID),
log.Err(err),
)
}
}
}
func (r *chainRouter) Disconnected(nodeID ids.NodeID) {
r.lock.Lock()
defer r.lock.Unlock()
r.connectedPeers.Remove(nodeID)
handlers := make([]handler.Handler, 0, len(r.chains))
for _, h := range r.chains {
handlers = append(handlers, h)
}
r.lock.Unlock()
r.log.Debug("peer disconnected",
log.Stringer("nodeID", nodeID),
)
for _, h := range handlers {
if err := h.Disconnected(context.Background(), nodeID); err != nil {
r.log.Debug("chain handler Disconnected failed",
log.Stringer("nodeID", nodeID),
log.Err(err),
)
}
}
}
func (r *chainRouter) Benched(chainID ids.ID, nodeID ids.NodeID) {
@@ -0,0 +1,90 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package node
import (
"context"
"sync"
"testing"
"github.com/stretchr/testify/require"
"github.com/luxfi/consensus/networking/handler"
"github.com/luxfi/constants"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/math/set"
"github.com/luxfi/node/version"
luxversion "github.com/luxfi/version"
)
// fakeVersionedHandler implements handler.Handler AND the versionedConnector
// capability, recording which path the router used and the version delivered.
type fakeVersionedHandler struct {
mu sync.Mutex
versionedHit bool
plainHit bool
gotAppVersion *luxversion.Application
}
func (h *fakeVersionedHandler) HandleInbound(context.Context, handler.Message) error { return nil }
func (h *fakeVersionedHandler) HandleOutbound(context.Context, handler.Message) error { return nil }
func (h *fakeVersionedHandler) Connected(context.Context, ids.NodeID) error {
h.mu.Lock()
defer h.mu.Unlock()
h.plainHit = true
return nil
}
func (h *fakeVersionedHandler) Disconnected(context.Context, ids.NodeID) error { return nil }
func (h *fakeVersionedHandler) ConnectedWithVersion(_ context.Context, _ ids.NodeID, v *luxversion.Application) error {
h.mu.Lock()
defer h.mu.Unlock()
h.versionedHit = true
h.gotAppVersion = v
return nil
}
// TestChainRouterConnectedDeliversConvertedVersion is the router half of the
// RED CRITICAL #1 fix: chainRouter.Connected must deliver the REAL peer version
// to a version-capable handler, converting it from the node's peer version type
// (github.com/luxfi/node/version) to the VM boundary type
// (github.com/luxfi/version, aka chain.VersionInfo). The old code dropped the
// version at dispatch (h.Connected(ctx, nodeID)); the fix routes through the
// versionedConnector capability so the real, converted version survives.
func TestChainRouterConnectedDeliversConvertedVersion(t *testing.T) {
require := require.New(t)
h := &fakeVersionedHandler{}
chainID := ids.GenerateTestID()
r := &chainRouter{
log: log.Noop(),
chains: map[ids.ID]handler.Handler{chainID: h},
connectedPeers: set.NewSet[ids.NodeID](1),
}
nodeID := ids.GenerateTestNodeID()
peerVersion := &version.Application{Name: "lux", Major: 1, Minor: 36, Patch: 27}
r.Connected(nodeID, peerVersion, constants.PrimaryNetworkID)
h.mu.Lock()
defer h.mu.Unlock()
require.True(h.versionedHit, "router must use the versioned capability path for a version-capable handler")
require.False(h.plainHit, "router must NOT fall back to the nil-version plain Connected")
require.NotNil(h.gotAppVersion, "handler must receive a non-nil converted version")
require.Equal("lux", h.gotAppVersion.Name)
require.Equal(1, h.gotAppVersion.Major)
require.Equal(36, h.gotAppVersion.Minor)
require.Equal(27, h.gotAppVersion.Patch)
}
// TestToAppVersionNilSafe documents that a nil peer version converts to nil
// (never a panic) — the conversion is defensive at the boundary.
func TestToAppVersionNilSafe(t *testing.T) {
require.Nil(t, toAppVersion(nil))
}
-265
View File
@@ -1,265 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package node
import (
"crypto/tls"
"net/netip"
"time"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/genesis/pkg/genesis"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/math/set"
"github.com/luxfi/node/server/http"
"github.com/luxfi/node/benchlist"
"github.com/luxfi/node/chains"
"github.com/luxfi/node/nets"
"github.com/luxfi/node/network"
"github.com/luxfi/node/trace"
"github.com/luxfi/node/upgrade"
"github.com/luxfi/node/vms/platformvm/txs/fee"
"github.com/luxfi/timer"
"github.com/luxfi/node/utils/profiler"
)
type APIIndexerConfig struct {
IndexAPIEnabled bool `json:"indexAPIEnabled"`
IndexAllowIncomplete bool `json:"indexAllowIncomplete"`
}
type TargeterConfig struct {
// VdrAlloc is the percentage of resource usage that is attributed to validators
// The range is [0, 1], defaults to 1 (100%)
VdrAlloc float64 `json:"vdrAlloc"`
// MaxNonVdrUsage is the maximum amount of resources that non-validators can use
// The range is [0, 1], defaults to 0
MaxNonVdrUsage float64 `json:"maxNonVdrUsage"`
// MaxNonVdrNodeUsage is the maximum amount of resources that a non-validator node can use
// The range is [0, 1], defaults to 0
MaxNonVdrNodeUsage float64 `json:"maxNonVdrNodeUsage"`
}
type HTTPConfig struct {
server.HTTPConfig
APIConfig `json:"apiConfig"`
HTTPHost string `json:"httpHost"`
HTTPPort uint16 `json:"httpPort"`
HTTPSEnabled bool `json:"httpsEnabled"`
HTTPSKey []byte `json:"-"`
HTTPSCert []byte `json:"-"`
HTTPAllowedOrigins []string `json:"httpAllowedOrigins"`
HTTPAllowedHosts []string `json:"httpAllowedHosts"`
ShutdownTimeout time.Duration `json:"shutdownTimeout"`
ShutdownWait time.Duration `json:"shutdownWait"`
}
type APIConfig struct {
APIIndexerConfig `json:"indexerConfig"`
// Enable/Disable APIs
AdminAPIEnabled bool `json:"adminAPIEnabled"`
InfoAPIEnabled bool `json:"infoAPIEnabled"`
KeystoreAPIEnabled bool `json:"keystoreAPIEnabled"`
MetricsAPIEnabled bool `json:"metricsAPIEnabled"`
HealthAPIEnabled bool `json:"healthAPIEnabled"`
}
type IPConfig struct {
PublicIP string `json:"publicIP"`
PublicIPResolutionService string `json:"publicIPResolutionService"`
PublicIPResolutionFreq time.Duration `json:"publicIPResolutionFreq"`
// The host portion of the address to listen on. The port to
// listen on will be sourced from IPPort.
//
// - If empty, listen on all interfaces (both ipv4 and ipv6).
// - If populated, listen only on the specified address.
ListenHost string `json:"listenHost"`
ListenPort uint16 `json:"listenPort"`
}
type StakingConfig struct {
genesis.StakingConfig
SybilProtectionEnabled bool `json:"sybilProtectionEnabled"`
PartialSyncPrimaryNetwork bool `json:"partialSyncPrimaryNetwork"`
StakingTLSCert tls.Certificate `json:"-"`
StakingSigningKey *bls.SecretKey `json:"-"`
SybilProtectionDisabledWeight uint64 `json:"sybilProtectionDisabledWeight"`
StakingKeyPath string `json:"stakingKeyPath"`
StakingCertPath string `json:"stakingCertPath"`
StakingSignerPath string `json:"stakingSignerPath"`
}
type StateSyncConfig struct {
StateSyncIDs []ids.NodeID `json:"stateSyncIDs"`
StateSyncIPs []netip.AddrPort `json:"stateSyncIPs"`
}
type BootstrapConfig struct {
// Timeout before emitting a warn log when connecting to bootstrapping beacons
BootstrapBeaconConnectionTimeout time.Duration `json:"bootstrapBeaconConnectionTimeout"`
// Max number of containers in an ancestors message sent by this node.
BootstrapAncestorsMaxContainersSent int `json:"bootstrapAncestorsMaxContainersSent"`
// This node will only consider the first [AncestorsMaxContainersReceived]
// containers in an ancestors message it receives.
BootstrapAncestorsMaxContainersReceived int `json:"bootstrapAncestorsMaxContainersReceived"`
// Max time to spend fetching a container and its
// ancestors while responding to a GetAncestors message
BootstrapMaxTimeGetAncestors time.Duration `json:"bootstrapMaxTimeGetAncestors"`
Bootstrappers []genesis.Bootstrapper `json:"bootstrappers"`
// Skip bootstrapping and start processing immediately
SkipBootstrap bool `json:"skipBootstrap"`
// Enable automining in POA mode
EnableAutomining bool `json:"enableAutomining"`
}
type DatabaseConfig struct {
// If true, all writes are to memory and are discarded at node shutdown.
ReadOnly bool `json:"readOnly"`
// Path to database
Path string `json:"path"`
// Name of the database type to use
Name string `json:"name"`
// Path to config file
Config []byte `json:"-"`
}
// Config contains all of the configurations of a Lux node.
type Config struct {
HTTPConfig `json:"httpConfig"`
IPConfig `json:"ipConfig"`
StakingConfig `json:"stakingConfig"`
fee.StaticConfig `json:"txFeeConfig"`
StateSyncConfig `json:"stateSyncConfig"`
BootstrapConfig `json:"bootstrapConfig"`
DatabaseConfig `json:"databaseConfig"`
UpgradeConfig upgrade.Config `json:"upgradeConfig"`
// Genesis information
GenesisBytes []byte `json:"-"`
UTXOAssetID ids.ID `json:"utxoAssetID"`
// ID of the network this node should connect to
NetworkID uint32 `json:"networkID"`
// Health
HealthCheckFreq time.Duration `json:"healthCheckFreq"`
// Network configuration
NetworkConfig network.Config `json:"networkConfig"`
AdaptiveTimeoutConfig timer.AdaptiveTimeoutConfig `json:"adaptiveTimeoutConfig"`
BenchlistConfig benchlist.Config `json:"benchlistConfig"`
ProfilerConfig profiler.Config `json:"profilerConfig"`
// LoggingConfig log.Config `json:"loggingConfig"` // log.Config doesn't exist
PluginDir string `json:"pluginDir"`
// DevMode enables local PoA + auto-mine mode (network-id=local, sybil-protection disabled)
DevMode bool `json:"devMode"`
// File Descriptor Limit
FdLimit uint64 `json:"fdLimit"`
// Metrics
MeterVMEnabled bool `json:"meterVMEnabled"`
// Delay between automatic block proposals when DevMode is set
DevBlockDelay time.Duration `json:"devBlockDelay"`
RouterHealthConfig HealthConfig `json:"routerHealthConfig"`
ConsensusShutdownTimeout time.Duration `json:"consensusShutdownTimeout"`
// Poll for new frontiers every [FrontierPollFrequency]
FrontierPollFrequency time.Duration `json:"consensusGossipFreq"`
// ConsensusAppConcurrency defines the maximum number of goroutines to
// handle App messages per chain.
ConsensusAppConcurrency int `json:"consensusAppConcurrency"`
TrackedChains set.Set[ids.ID] `json:"trackedChains"`
TrackAllChains bool `json:"trackAllChains"`
NetConfigs map[ids.ID]nets.Config `json:"chainConfigs"`
ChainConfigs map[string]chains.ChainConfig `json:"-"`
ChainAliases map[ids.ID][]string `json:"chainAliases"`
VMAliases map[ids.ID][]string `json:"vmAliases"`
// Halflife to use for the processing requests tracker.
// Larger halflife --> usage metrics change more slowly.
SystemTrackerProcessingHalflife time.Duration `json:"systemTrackerProcessingHalflife"`
// Frequency to check the real resource usage of tracked processes.
// More frequent checks --> usage metrics are more accurate, but more
// expensive to track
SystemTrackerFrequency time.Duration `json:"systemTrackerFrequency"`
// Halflife to use for the cpu tracker.
// Larger halflife --> cpu usage metrics change more slowly.
SystemTrackerCPUHalflife time.Duration `json:"systemTrackerCPUHalflife"`
// Halflife to use for the disk tracker.
// Larger halflife --> disk usage metrics change more slowly.
SystemTrackerDiskHalflife time.Duration `json:"systemTrackerDiskHalflife"`
CPUTargeterConfig TargeterConfig `json:"cpuTargeterConfig"`
DiskTargeterConfig TargeterConfig `json:"diskTargeterConfig"`
RequiredAvailableDiskSpace uint64 `json:"requiredAvailableDiskSpace"`
WarningThresholdAvailableDiskSpace uint64 `json:"warningThresholdAvailableDiskSpace"`
TraceConfig trace.Config `json:"traceConfig"`
// See comment on [UseCurrentHeight] in platformvm.Config
UseCurrentHeight bool `json:"useCurrentHeight"`
// ProvidedFlags contains all the flags set by the user
ProvidedFlags map[string]interface{} `json:"-"`
// ChainDataDir is the root path for per-chain directories where VMs can
// write arbitrary data.
ChainDataDir string `json:"chainDataDir"`
// ImportChainData is the path to import blockchain data from another chain
ImportChainData string `json:"importChainData"`
// Path to write process context to (including PID, API URI, and
// staking address).
ProcessContextFilePath string `json:"processContextFilePath"`
// POA Mode Configuration
POAModeEnabled bool `json:"poaModeEnabled"`
POASingleNodeMode bool `json:"poaSingleNodeMode"`
POAMinBlockTime time.Duration `json:"poaMinBlockTime"`
POAAuthorizedNodes []string `json:"poaAuthorizedNodes"`
// Low Memory Configuration
LowMemoryEnabled bool `json:"lowMemoryEnabled"`
DBCacheSize uint64 `json:"dbCacheSize"`
DBMemtableSize uint64 `json:"dbMemtableSize"`
StateCacheSize uint64 `json:"stateCacheSize"`
BlockCacheSize uint64 `json:"blockCacheSize"`
DisableBloomFilters bool `json:"disableBloomFilters"`
LazyChainLoading bool `json:"lazyChainLoading"`
SingleValidatorMode bool `json:"singleValidatorMode"`
// Logging
Log log.Logger `json:"-"`
}
+4
View File
@@ -1354,6 +1354,10 @@ func (n *Node) initChainManager(utxoAssetID ids.ID) error {
SybilProtectionEnabled: n.Config.SybilProtectionEnabled,
StakingTLSSigner: n.StakingTLSSigner,
StakingTLSCert: n.StakingTLSCert,
StakingMLDSASigner: n.Config.StakingConfig.StakingMLDSA,
StakingMLDSAPub: n.Config.StakingConfig.StakingMLDSAPub,
ProposerWindowDuration: n.Config.ProposerWindowDuration,
ProposerMinBlockDelay: n.Config.ProposerMinBlockDelay,
StakingBLSKey: n.Config.StakingSigningKey,
Log: n.Log,
LogFactory: n.LogFactory,
+24 -12
View File
@@ -114,18 +114,29 @@ var CoreVMs = map[ids.ID]CoreVM{
// today's opt-in-flag behavior) while the gate itself remains fail-closed.
//
// C (evm) and the remaining app VMs are plugin-loaded but ungated today.
//
// Declaring a VM here is a statement of intent, not a guarantee that a binary
// exists: the registry scan simply finds nothing and the chain cannot start.
// fhevm (F-Chain) is exactly that case today — see its entry below.
var OptionalVMs = map[ids.ID]PluginSpec{
constants.DexVMID: {Name: "dexvm", RequiredNFT: &NFTRequirement{Collection: "dex-operator", GroupID: 0}},
constants.BridgeVMID: {Name: "bridgevm", RequiredNFT: &NFTRequirement{Collection: "bridge-operator", GroupID: 0}},
constants.EVMID: {Name: "evm"},
constants.AIVMID: {Name: "aivm"},
constants.GraphVMID: {Name: "graphvm"},
constants.IdentityVMID: {Name: "identityvm"},
constants.KeyVMID: {Name: "keyvm"},
constants.OracleVMID: {Name: "oraclevm"},
constants.RelayVMID: {Name: "relayvm"},
constants.MPCVMID: {Name: "mpcvm"},
constants.FHEVMID: {Name: "fhevm"},
constants.DexVMID: {Name: "dexvm", RequiredNFT: &NFTRequirement{Collection: "dex-operator", GroupID: 0}},
constants.BridgeVMID: {Name: "bridgevm", RequiredNFT: &NFTRequirement{Collection: "bridge-operator", GroupID: 0}},
constants.EVMID: {Name: "evm"},
constants.AIVMID: {Name: "aivm"},
constants.GraphVMID: {Name: "graphvm"},
constants.IdentityVMID: {Name: "identityvm"},
constants.KeyVMID: {Name: "keyvm"},
constants.OracleVMID: {Name: "oraclevm"},
constants.RelayVMID: {Name: "relayvm"},
constants.MPCVMID: {Name: "mpcvm"},
// F-Chain. Reserved ID, no shipping VM: luxfi/chains has no fhevm/
// directory, so `make` produces no fhevm binary and this scan always comes
// up empty. The FHE runtime library lives at luxfi/chains/mpcvm/fhe as a
// package inside mpcvm, not as a standalone chain. F-Chain is a spec
// (LP-8200, LP-167). Kept declared so the intent stays visible and the ID
// stays reserved — remove it only when F-Chain is cancelled, not merely
// because it is unbuilt.
constants.FHEVMID: {Name: "fhevm"},
}
func init() {
@@ -155,7 +166,8 @@ func assertRegistriesDisjoint() error {
// registerCoreVMs registers the in-process core VMs that carry a concrete
// factory (Q, Z). P and X are registered separately in node.go because their
// factories require live node dependencies (RegisteredInNodeGo=true in
// CoreVMs). The optional chain VMs (A/B/C/D/G/I/K/O/R/T) are NEVER registered
// CoreVMs). The optional chain VMs (A/B/C/D/F/G/I/K/M/O/R — the keys of
// OptionalVMs; there is no T, LP-134 dissolved it) are NEVER registered
// here — they load from PluginDir via the VMRegistry scan, exactly like any
// other plugin. Registering an optional VM in-process would shadow its plugin
// (the registry skips any VMID the manager already has — vms/registry/
-22
View File
@@ -1,22 +0,0 @@
FROM bufbuild/buf:1.26.1 AS builder
FROM ubuntu:20.04
RUN apt-get update && apt -y install bash curl unzip git
WORKDIR /opt
RUN \
curl -L https://golang.org/dl/go1.24.5.linux-amd64.tar.gz > golang.tar.gz && \
mkdir golang && \
tar -zxvf golang.tar.gz -C golang/
ENV PATH="${PATH}:/opt/golang/go/bin"
COPY --from=builder /usr/local/bin/buf /usr/local/bin/
# any version changes here should also be bumped in scripts/protobuf_codegen.sh
RUN \
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30.0 && \
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0
ENV PATH="${PATH}:/root/go/bin/"
-36
View File
@@ -1,36 +0,0 @@
# Lux gRPC
Now Serving: **Protocol Version 42**
Protobuf files are hosted at
[https://buf.build/luxfi/lux](https://buf.build/luxfi/lux) and
can be used as dependencies in other projects.
Protobuf linting and generation for this project is managed by
[buf](https://github.com/bufbuild/buf).
Please find installation instructions on
[https://docs.buf.build/installation/](https://docs.buf.build/installation/).
Any changes made to proto definition can be updated by running
`protobuf_codegen.sh` located in the `scripts/` directory of Lux Node.
`buf` Quickstart
[https://buf.build/docs/cli/quickstart](https://buf.build/docs/cli/quickstart)
## Protocol Version Compatibility
The protobuf definitions and generated code are versioned based on the
[RPCChainVMProtocol](../version/version.go#L13) defined for the RPCChainVM.
Many versions of a Lux client can use the same
[RPCChainVMProtocol](../version/version.go#L13). But each Lux client and
chain VM must use the same protocol version to be compatible.
## Publishing to Buf Schema Registry
- Checkout appropriate tag in Lux Node `git checkout v1.10.1`
- Change to proto/ directory `cd proto`.
- Publish new tag to buf registry. `buf push -t v26`
Note: Publishing requires auth to the luxfi org in buf
https://buf.build/luxfi/repositories
-8
View File
@@ -1,8 +0,0 @@
version: v1
plugins:
- name: go
out: pb
opt: paths=source_relative
- name: go-grpc
out: pb
opt: paths=source_relative
-36
View File
@@ -1,36 +0,0 @@
# Lux gRPC
Now Serving: **Protocol Version 42**
Protobuf files are hosted at
[https://buf.build/luxfi/lux](https://buf.build/luxfi/lux) and
can be used as dependencies in other projects.
Protobuf linting and generation for this project is managed by
[buf](https://github.com/bufbuild/buf).
Please find installation instructions on
[https://docs.buf.build/installation/](https://docs.buf.build/installation/).
Any changes made to proto definition can be updated by running
`protobuf_codegen.sh` located in the `scripts/` directory of Lux Node.
`buf` Quickstart
[https://buf.build/docs/cli/quickstart](https://buf.build/docs/cli/quickstart)
## Protocol Version Compatibility
The protobuf definitions and generated code are versioned based on the
[RPCChainVMProtocol](../version/version.go#L13) defined for the RPCChainVM.
Many versions of a Lux client can use the same
[RPCChainVMProtocol](../version/version.go#L13). But each Lux client and
chain VM must use the same protocol version to be compatible.
## Publishing to Buf Schema Registry
- Checkout appropriate tag in Lux Node `git checkout v1.10.1`
- Change to proto/ directory `cd proto`.
- Publish new tag to buf registry. `buf push -t v26`
Note: Publishing requires auth to the luxfi org in buf
https://buf.build/luxfi/repositories
-30
View File
@@ -1,30 +0,0 @@
version: v1
name: buf.build/luxfi/lux
build:
excludes: []
breaking:
use:
- FILE
# deps removed - now using local io/metric/client/metrics.proto
lint:
use:
- STANDARD
except:
- SERVICE_SUFFIX # service requirement of <name>+Service
- RPC_REQUEST_STANDARD_NAME # explicit <rpc>+Request naming
- RPC_RESPONSE_STANDARD_NAME # explicit <rpc>+Response naming
- PACKAGE_VERSION_SUFFIX # versioned naming <service>.v1beta
ignore:
# TODO: how will fixing this affect functionality. Multiple fields are used as the request
# or response type for multiple RPCs
- aliasreader/aliasreader.proto
- net/conn/conn.proto
# Third-party proto from prometheus/client_model - don't lint
- io/metric/client/metrics.proto
# allows RPC requests or responses to be google.protobuf.Empty messages. This can be set if you
# want to allow messages to be void forever, that is they will never take any parameters.
rpc_allow_google_protobuf_empty_requests: true
rpc_allow_google_protobuf_empty_responses: true
# allows the same message type to be used for a single RPC's request and response type.
# TODO: this should not be tolerated and if it is only perscriptivly.
rpc_allow_same_request_response: true
-25
View File
@@ -1,25 +0,0 @@
#!/bin/bash
# Rename types, methods, and functions to remove "App" prefix
# Targeted at p2p message types and builder functions
# 1. Rename Message Types and Fields (Getters)
find . -name "*.go" -type f -print0 | xargs -0 sed -i '' \
-e 's/AppRequest/Request/g' \
-e 's/AppResponse/Response/g' \
-e 's/AppGossip/Gossip/g' \
-e 's/AppError/Error/g' \
-e 's/GetAppRequest/GetRequest/g' \
-e 's/GetAppResponse/GetResponse/g' \
-e 's/GetAppGossip/GetGossip/g' \
-e 's/GetAppError/GetError/g'
# 2. Rename Builder Functions (Inbound/Outbound)
# Note: s/AppRequest/Request/g above already handled the suffix.
# Now we need to handle prefixes if they still exist.
# e.g. InboundAppRequest -> InboundRequest (if AppRequest matched first, it became InboundRequest)
# Check: InboundAppRequest -> InboundRequest.
# So IsInboundAppRequest -> IsInboundRequest?
# We should just ensure "InboundApp" -> "Inbound" (for any other patterns)
find . -name "*.go" -type f -print0 | xargs -0 sed -i '' \
-e 's/InboundApp/Inbound/g' \
-e 's/OutboundApp/Outbound/g'
-30
View File
@@ -1,30 +0,0 @@
#!/bin/bash
# Replace package declarations in moved files
# Ensure we are fixing package names for moved files
# vm/manager was 'package manager', so it fits node/vms/manager
# vm/rpc was 'package rpc' (likely), needs to be 'package rpcchainvm'
if [ -d "vms/manager" ]; then
sed -i '' 's/^package.*/package manager/' vms/manager/*.go
fi
if [ -d "vms/rpcchainvm" ]; then
sed -i '' 's/^package.*/package rpcchainvm/' vms/rpcchainvm/*.go
fi
# Global replacements
DIRS=". ../precompile ../coreth ../evm ../rpc ../wallet ../staking"
for dir in $DIRS; do
if [ -d "$dir" ]; then
echo "Processing $dir..."
find "$dir" -name "*.go" -type f -print0 | xargs -0 sed -i '' \
-e 's|github.com/luxfi/consensus/runtime|github.com/luxfi/runtime|g' \
-e 's|github.com/luxfi/consensus/validator|github.com/luxfi/validators|g' \
-e 's|github.com/luxfi/consensus/version|github.com/luxfi/version|g' \
-e 's|github.com/luxfi/vm/manager|github.com/luxfi/node/vms/manager|g' \
-e 's|github.com/luxfi/vm/rpc|github.com/luxfi/node/vms/rpcchainvm|g' \
-e 's|github.com/luxfi/vm/chains|github.com/luxfi/node/chains|g' \
-e 's|version\.Current()|version.CurrentApp|g' \
-e 's|consensusversion\.Current()|version.CurrentApp|g'
fi
done
+13 -5
View File
@@ -54,8 +54,16 @@ source "${REPO_ROOT}"/scripts/constants.sh
# Determine the git commit hash to use for the build
source "${REPO_ROOT}"/scripts/git_commit.sh
# Configure build based on profile
tags=""
# Configure build based on profile.
#
# `metrics` is a base tag, not a profile choice: without it luxfi/metric's
# NewRegistry() resolves to the no-op registry (registry_noop.go, //go:build
# !metrics), every metric registers into a black hole, and /v1/metrics answers
# 200 with a zero-byte body — while --api-metrics-enabled=true still reports
# metrics as on. Whether metrics are served is the runtime flag's decision
# alone; the build must not silently overrule it.
base_tags="metrics"
tags="${base_tags}"
ldflags="-X github.com/luxfi/node/version.GitCommit=$git_commit \
-X github.com/luxfi/node/version.VersionMajor=$version_major \
-X github.com/luxfi/node/version.VersionMinor=$version_minor \
@@ -67,7 +75,7 @@ upx_compress=false
case "${profile}" in
minimal)
echo "Profile: minimal (ZAP, all VMs, NAT, stripped)"
tags="nattraversal"
tags="${base_tags},nattraversal"
strip_flags="-s -w"
;;
core)
@@ -77,7 +85,7 @@ case "${profile}" in
;;
full)
echo "Profile: full (gRPC+ZAP, all VMs, all features, stripped)"
tags="grpc,nattraversal,zxcvbn,metrics"
tags="${base_tags},grpc,nattraversal,zxcvbn"
strip_flags="-s -w"
;;
dev)
@@ -86,7 +94,7 @@ case "${profile}" in
;;
tiny)
echo "Profile: tiny (all VMs, NAT + UPX compressed)"
tags="nattraversal"
tags="${base_tags},nattraversal"
strip_flags="-s -w"
upx_compress=true
;;
-60
View File
@@ -1,60 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
if ! [[ "$0" =~ scripts/protobuf_codegen.sh ]]; then
echo "must be run from repository root"
exit 255
fi
# the versions here should match those of the binaries installed in the nix dev shell
## ensure the correct version of "buf" is installed
BUF_VERSION='1.47.2'
if [[ $(buf --version | cut -f2 -d' ') != "${BUF_VERSION}" ]]; then
echo "could not find buf ${BUF_VERSION}, is it installed + in PATH?"
exit 255
fi
## ensure the correct version of "protoc-gen-go" is installed
PROTOC_GEN_GO_VERSION='v1.35.1'
if [[ $(protoc-gen-go --version | cut -f2 -d' ') != "${PROTOC_GEN_GO_VERSION}" ]]; then
echo "could not find protoc-gen-go ${PROTOC_GEN_GO_VERSION}, is it installed + in PATH?"
exit 255
fi
## ensure the correct version of "protoc-gen-go-grpc" is installed
PROTOC_GEN_GO_GRPC_VERSION='1.3.0'
if [[ $(protoc-gen-go-grpc --version | cut -f2 -d' ') != "${PROTOC_GEN_GO_GRPC_VERSION}" ]]; then
echo "could not find protoc-gen-go-grpc ${PROTOC_GEN_GO_GRPC_VERSION}, is it installed + in PATH?"
exit 255
fi
BUF_MODULES=("proto" "connectproto")
REPO_ROOT=$PWD
for BUF_MODULE in "${BUF_MODULES[@]}"; do
TARGET=$REPO_ROOT/$BUF_MODULE
if [ -n "${1:-}" ]; then
TARGET="$1"
fi
# move to buf module directory
cd "$TARGET"
echo "Generating for buf module $BUF_MODULE"
echo "Running protobuf fmt for..."
buf format -w
echo "Running protobuf lint check..."
if ! buf lint; then
echo "ERROR: protobuf linter failed"
exit 1
fi
echo "Re-generating protobuf..."
if ! buf generate; then
echo "ERROR: protobuf generation failed"
exit 1
fi
done
+1 -1
View File
@@ -60,7 +60,7 @@ PLUGINS=(
r5m1ujrmXxVcQetG3CQfuDLHp2RHKh6vCDaFgBRQfUcTZh7eS # oraclevm
ry9Sg8rZdT26iEKvJDmC2wkESs4SDKgZEhk5BgLSwg1EpcNug # quantumvm
sP6dLqrrBR9w3soP18fbJ3YzZecZdD7DDdfH2cFhhLq7Hy9bz # relayvm
tGVBwRxpmD2aFdg3iYjgRvrCe8Jcmq9UNKxyHMus2NZ8WcD8t # mpcvm
qCURact1n41FcoNBch8iMVBwc9AWie48D118ZNJ5tBdWrvryS # mpcvm
vv3qPfyTVXZ5ArRZA9Jh4hbYDTBe43f7sgQg4CHfNg1rnnvX9 # zkvm
)
+5 -1
View File
@@ -19,6 +19,7 @@ import (
"net/http"
"os"
"github.com/valyala/fasthttp/fasthttpadaptor"
zaphttp "github.com/zap-proto/http"
log "github.com/luxfi/log"
@@ -53,7 +54,10 @@ func startZapRPCListener(logger log.Logger, handler http.Handler, addr string) *
if addr == "" {
return nil
}
srv := &zaphttp.Server{Addr: addr, Handler: handler}
// zap-proto/http >= v0.2.0 takes a fasthttp.RequestHandler. Adapt the very
// same net/http handler chain the HTTP listener serves, so the two
// transports stay behaviourally identical — only the wire encoding differs.
srv := &zaphttp.Server{Addr: addr, Handler: fasthttpadaptor.NewFastHTTPHandler(handler)}
go func() {
logger.Info("ZAP-RPC API listening", log.UserString("addr", addr))
if err := srv.ListenAndServe(); err != nil {
+24 -9
View File
@@ -9,6 +9,7 @@ import (
"testing"
"time"
"github.com/valyala/fasthttp"
zaphttp "github.com/zap-proto/http"
log "github.com/luxfi/log"
@@ -22,8 +23,8 @@ func TestZapRPCListenAddr(t *testing.T) {
val string
want string
}{
{set: false, want: ""}, // unset → disabled (default)
{set: true, val: "", want: ""}, // empty → disabled
{set: false, want: ""}, // unset → disabled (default)
{set: true, val: "", want: ""}, // empty → disabled
{set: true, val: "off", want: ""},
{set: true, val: "OFF", want: ""},
{set: true, val: "0", want: ""},
@@ -74,18 +75,32 @@ func TestStartZapRPCListener_RoundTrip(t *testing.T) {
// Give the goroutine a beat to bind.
time.Sleep(150 * time.Millisecond)
client := &http.Client{Transport: zaphttp.NewTransport(addr)}
resp, err := client.Post("http://"+addr+"/v1/bc/C/rpc", "application/json", nil)
if err != nil {
// zap-proto/http >= v0.2.0 speaks fasthttp on both ends: Transport.Do takes
// a fasthttp request/response pair rather than implementing
// http.RoundTripper. The round trip being asserted is unchanged — a real
// ZAP request over the wire must reach the net/http handler the listener
// was given, through the fasthttpadaptor bridge in startZapRPCListener.
// v0.3.0 replaced NewTransport with Dial, mirroring net.Dial: the network is
// a VALUE ("tcp", "unix") rather than a family of constructors.
transport := zaphttp.Dial("tcp", addr)
defer transport.CloseIdleConnections()
req, resp := fasthttp.AcquireRequest(), fasthttp.AcquireResponse()
defer fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(resp)
req.SetRequestURI("http://" + addr + "/v1/bc/C/rpc")
req.Header.SetMethod(http.MethodPost)
req.Header.SetContentType("application/json")
if err := transport.Do(req, resp); err != nil {
t.Fatalf("ZAP round-trip POST failed: %v", err)
}
defer resp.Body.Close()
got, _ := io.ReadAll(resp.Body)
if string(got) != body {
if got := string(resp.Body()); got != body {
t.Fatalf("ZAP round-trip body = %q, want %q", got, body)
}
if ct := resp.Header.Get("Content-Type"); ct != "application/json" {
if ct := string(resp.Header.ContentType()); ct != "application/json" {
t.Fatalf("ZAP round-trip Content-Type = %q, want application/json", ct)
}
}
+126
View File
@@ -0,0 +1,126 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// logger_level_test.go — admin.setLoggerLevel / admin.getLoggerLevel must actually
// move a live logger's level.
//
// They did not. SetLoggerLevel computed the logger names and threw them away
// (`loggerNames := a.getLoggerNames(...); _ = loggerNames`) and getLogLevels returned
// an empty map unconditionally, so BOTH endpoints answered 200 OK having done nothing.
// That is why the 2026-07-28 devnet/testnet build-loop diagnosis had to be run off boot
// logs: raising a running node's log level was impossible.
//
// The tests drive the REAL log.Factory (the same one the node builds), not a double,
// so a passing SetLoggerLevel means the level the logger actually filters on moved.
package admin
import (
"context"
"testing"
"github.com/stretchr/testify/require"
apiadmin "github.com/luxfi/api/admin"
"github.com/luxfi/log"
)
// newLevelTestService returns an admin service over a real log.Factory that already
// holds one registered logger — the shape the node runs in.
func newLevelTestService(t *testing.T, loggerName string) *Service {
t.Helper()
factory := log.NewFactory()
t.Cleanup(factory.Close)
_, err := factory.Make(loggerName)
require.NoError(t, err)
return &Service{Config: Config{Log: log.Noop(), LogFactory: factory}}
}
// TestSetLoggerLevel_MovesTheLevelAndGetReportsIt is the regression: set, then read
// back through the API. Before the fix the set was discarded and the get returned an
// empty map, so both halves silently lied.
func TestSetLoggerLevel_MovesTheLevelAndGetReportsIt(t *testing.T) {
require := require.New(t)
ctx := context.Background()
svc := newLevelTestService(t, "C")
_, err := svc.SetLoggerLevel(ctx, &apiadmin.SetLoggerLevelArgs{
LoggerName: "C",
LogLevel: "debug",
DisplayLevel: "error",
})
require.NoError(err)
reply, err := svc.GetLoggerLevel(ctx, &apiadmin.GetLoggerLevelArgs{LoggerName: "C"})
require.NoError(err)
require.Equal(
map[string]apiadmin.LogAndDisplayLevels{"C": {
LogLevel: log.DebugLevel.String(),
DisplayLevel: log.ErrorLevel.String(),
}},
reply.LoggerLevels,
"setLoggerLevel must move the live logger's level and getLoggerLevel must report it",
)
// The factory is the single source of truth — assert against it directly too, so a
// getLoggerLevel that merely echoed the request back could not pass this test.
logLevel, err := svc.LogFactory.GetLogLevel("C")
require.NoError(err)
require.Equal(log.DebugLevel, logLevel)
}
// TestSetLoggerLevel_OneLevelAtATime pins that omitting a level leaves it alone rather
// than resetting it to the zero Level.
func TestSetLoggerLevel_OneLevelAtATime(t *testing.T) {
require := require.New(t)
ctx := context.Background()
svc := newLevelTestService(t, "node")
_, err := svc.SetLoggerLevel(ctx, &apiadmin.SetLoggerLevelArgs{
LoggerName: "node", LogLevel: "trace", DisplayLevel: "warn",
})
require.NoError(err)
// Only displayLevel this time — logLevel must survive.
_, err = svc.SetLoggerLevel(ctx, &apiadmin.SetLoggerLevelArgs{
LoggerName: "node", DisplayLevel: "fatal",
})
require.NoError(err)
reply, err := svc.GetLoggerLevel(ctx, &apiadmin.GetLoggerLevelArgs{LoggerName: "node"})
require.NoError(err)
require.Equal(log.TraceLevel.String(), reply.LoggerLevels["node"].LogLevel)
require.Equal(log.FatalLevel.String(), reply.LoggerLevels["node"].DisplayLevel)
}
// TestLoggerLevel_RejectsUnservableAndInvalidArgs — every refusal is explicit. log.Factory
// addresses loggers BY NAME and exposes no enumeration, so an empty name cannot be served;
// returning 200 OK for it is the bug, not the contract.
func TestLoggerLevel_RejectsUnservableAndInvalidArgs(t *testing.T) {
require := require.New(t)
ctx := context.Background()
svc := newLevelTestService(t, "C")
_, err := svc.SetLoggerLevel(ctx, &apiadmin.SetLoggerLevelArgs{LogLevel: "debug"})
require.ErrorIs(err, errNoLoggerName)
_, err = svc.GetLoggerLevel(ctx, &apiadmin.GetLoggerLevelArgs{})
require.ErrorIs(err, errNoLoggerName)
_, err = svc.SetLoggerLevel(ctx, &apiadmin.SetLoggerLevelArgs{LoggerName: "C"})
require.ErrorIs(err, errNoLogLevel)
// An unparseable level must be refused BEFORE anything is mutated.
before, err := svc.LogFactory.GetLogLevel("C")
require.NoError(err)
_, err = svc.SetLoggerLevel(ctx, &apiadmin.SetLoggerLevelArgs{LoggerName: "C", LogLevel: "loud"})
require.Error(err)
after, err := svc.LogFactory.GetLogLevel("C")
require.NoError(err)
require.Equal(before, after, "a rejected level must leave the logger untouched")
}
+61 -8
View File
@@ -40,6 +40,7 @@ const (
var (
errAliasTooLong = errors.New("alias length is too long")
errNoLogLevel = errors.New("need to specify either displayLevel or logLevel")
errNoLoggerName = errors.New("need to specify loggerName: loggers are addressed by name and cannot be enumerated")
)
// ChainTracker is the interface for tracking chains at runtime.
@@ -209,11 +210,39 @@ func (a *Service) SetLoggerLevel(ctx context.Context, args *apiadmin.SetLoggerLe
return nil, errNoLogLevel
}
// Parse before mutating: a rejected level must leave every logger untouched.
var logLevel, displayLevel log.Level
if args.LogLevel != "" {
var err error
if logLevel, err = log.ToLevel(args.LogLevel); err != nil {
return nil, err
}
}
if args.DisplayLevel != "" {
var err error
if displayLevel, err = log.ToLevel(args.DisplayLevel); err != nil {
return nil, err
}
}
loggerNames, err := a.getLoggerNames(args.LoggerName)
if err != nil {
return nil, err
}
a.lock.Lock()
defer a.lock.Unlock()
loggerNames := a.getLoggerNames(args.LoggerName)
_ = loggerNames
for _, name := range loggerNames {
// Only the levels the caller supplied — an omitted level keeps its value
// instead of being reset to the zero Level.
if args.LogLevel != "" {
a.LogFactory.SetLogLevel(name, logLevel)
}
if args.DisplayLevel != "" {
a.LogFactory.SetDisplayLevel(name, displayLevel)
}
}
return &apiadmin.EmptyReply{}, nil
}
@@ -225,10 +254,14 @@ func (a *Service) GetLoggerLevel(ctx context.Context, args *apiadmin.GetLoggerLe
log.String("loggerName", args.LoggerName),
)
loggerNames, err := a.getLoggerNames(args.LoggerName)
if err != nil {
return nil, err
}
a.lock.RLock()
defer a.lock.RUnlock()
loggerNames := a.getLoggerNames(args.LoggerName)
loggerLevels, err := a.getLogLevels(loggerNames)
if err != nil {
return nil, err
@@ -401,15 +434,35 @@ func (a *Service) GetTrackedChains(ctx context.Context) (*apiadmin.GetTrackedCha
return &apiadmin.GetTrackedChainsReply{TrackedChains: trackedChains}, nil
}
func (a *Service) getLoggerNames(loggerName string) []string {
if len(loggerName) == 0 {
return []string{}
// getLoggerNames resolves the loggerName argument to the loggers to act on — the one
// place either logger-level endpoint decides what it is addressing.
//
// log.Factory addresses loggers BY NAME and exposes no enumeration, so the "every
// logger" form (an empty name) cannot be served. Refuse it explicitly: answering 200 OK
// while doing nothing is what made these endpoints unusable.
func (a *Service) getLoggerNames(loggerName string) ([]string, error) {
if loggerName == "" {
return nil, errNoLoggerName
}
return []string{loggerName}
return []string{loggerName}, nil
}
func (a *Service) getLogLevels(loggerNames []string) (map[string]apiadmin.LogAndDisplayLevels, error) {
loggerLevels := make(map[string]apiadmin.LogAndDisplayLevels)
loggerLevels := make(map[string]apiadmin.LogAndDisplayLevels, len(loggerNames))
for _, name := range loggerNames {
logLevel, err := a.LogFactory.GetLogLevel(name)
if err != nil {
return nil, err
}
displayLevel, err := a.LogFactory.GetDisplayLevel(name)
if err != nil {
return nil, err
}
loggerLevels[name] = apiadmin.LogAndDisplayLevels{
LogLevel: logLevel.String(),
DisplayLevel: displayLevel.String(),
}
}
return loggerLevels, nil
}
+16 -10
View File
@@ -5,11 +5,10 @@ package health
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"github.com/gorilla/rpc/v2"
apihealth "github.com/luxfi/api/health"
@@ -56,17 +55,24 @@ func NewGetHandler(reporter func(tags ...string) (map[string]apihealth.Result, b
// If a health check has failed, we should return a 503.
w.WriteHeader(http.StatusServiceUnavailable)
}
// Buffer the reply — a streaming encoder that errors part-way
// (jsonv2 rejects invalid UTF-8, and check Details may embed raw
// chain-ID bytes) leaves the client a torn body whose
// Content-Length matches the truncation. Buffering makes the
// reply atomic; AllowInvalidUTF8 turns binary detail bytes into
// replacement runes instead of an encode error.
// One encoder for one wire type: apihealth.APIReply is defined with
// encoding/json tags and carries a time.Duration per check, so it is
// encoding/json that defines its representation. The POST (jsonrpc)
// path already encodes it through that codec; encoding it here the
// same way is what makes GET and POST agree. jsonv2 cannot encode
// this type at all — time.Duration has no default representation
// there — so every GET reply used to degrade to the error fallback
// below. encoding/json also replaces invalid UTF-8 (check Details
// may embed raw chain-ID bytes) with U+FFFD rather than failing.
//
// Buffer first: a streaming encoder that errors part-way would leave
// the client a torn body whose Content-Length matches the
// truncation. Buffering makes the reply atomic.
var buf bytes.Buffer
err := json.MarshalWrite(&buf, apihealth.APIReply{
err := json.NewEncoder(&buf).Encode(apihealth.APIReply{
Checks: checks,
Healthy: healthy,
}, jsontext.AllowInvalidUTF8(true))
})
if err != nil {
buf.Reset()
fmt.Fprintf(&buf, `{"healthy":%t,"error":"health reply encode failed"}`, healthy)
+103
View File
@@ -0,0 +1,103 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package health
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/require"
apihealth "github.com/luxfi/api/health"
)
// The GET handler must emit the real check set. It previously encoded through
// jsonv2, which cannot represent apihealth.Result.Duration (a time.Duration),
// so every reply on every node degraded to {"healthy":…,"error":"health reply
// encode failed"} while the status code still looked correct — k8s probes
// passed and operators saw nothing.
func TestGetHandlerEncodesDurationBearingChecks(t *testing.T) {
require := require.New(t)
checks := map[string]apihealth.Result{
"bls": {
Details: "node has the correct BLS key",
Duration: 134597 * time.Nanosecond,
Timestamp: time.Unix(1753479996, 0).UTC(),
},
}
h := NewGetHandler(func(...string) (map[string]apihealth.Result, bool) {
return checks, true
})
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/v1/health", nil))
require.Equal(http.StatusOK, rec.Code)
var reply apihealth.APIReply
require.NoError(json.Unmarshal(rec.Body.Bytes(), &reply))
require.True(reply.Healthy)
require.Len(reply.Checks, 1)
require.Equal(134597*time.Nanosecond, reply.Checks["bls"].Duration)
require.NotContains(rec.Body.String(), "health reply encode failed")
}
// An unhealthy node must still return the full diagnostic body alongside the
// 503 — the body is the only thing that says *why*.
func TestGetHandlerUnhealthyStillCarriesChecks(t *testing.T) {
require := require.New(t)
errMsg := "network layer is unhealthy reason: primary network validator has no inbound connections"
checks := map[string]apihealth.Result{
"network": {
Details: map[string]any{"connectedPeers": 4},
Error: &errMsg,
Duration: 40357 * time.Nanosecond,
ContiguousFailures: 31997,
},
}
h := NewGetHandler(func(...string) (map[string]apihealth.Result, bool) {
return checks, false
})
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/v1/health", nil))
require.Equal(http.StatusServiceUnavailable, rec.Code)
var reply apihealth.APIReply
require.NoError(json.Unmarshal(rec.Body.Bytes(), &reply))
require.False(reply.Healthy)
require.Equal(&errMsg, reply.Checks["network"].Error)
require.Equal(int64(31997), reply.Checks["network"].ContiguousFailures)
}
// Check Details may embed raw chain-ID bytes. Invalid UTF-8 must degrade to
// replacement runes, never to an encode failure that drops the whole reply.
func TestGetHandlerInvalidUTF8InDetailsDoesNotDropReply(t *testing.T) {
require := require.New(t)
checks := map[string]apihealth.Result{
"database": {
Details: string([]byte{0xff, 0xfe, 0x00}),
Duration: 19137 * time.Nanosecond,
},
}
h := NewGetHandler(func(...string) (map[string]apihealth.Result, bool) {
return checks, true
})
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/v1/health", nil))
require.Equal(http.StatusOK, rec.Code)
var reply apihealth.APIReply
require.NoError(json.Unmarshal(rec.Body.Bytes(), &reply))
require.Len(reply.Checks, 1)
require.NotContains(rec.Body.String(), "health reply encode failed")
}
+3
View File
@@ -12,6 +12,7 @@ github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4=
github.com/StephenButtolph/canoto v0.17.3 h1:lvsnYD4b96vD1knnmp1xCmZqfYpY/jSeRozGdOfdvGI=
github.com/StephenButtolph/canoto v0.17.3/go.mod h1:IcnAHC6nJUfQFVR9y60ko2ecUqqHHSB6UwI9NnBFZnE=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/chroma/v2 v2.17.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI=
github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU=
@@ -56,6 +57,7 @@ github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucV
github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/firefart/nonamedreturns v1.0.6 h1:vmiBcKV/3EqKY3ZiPxCINmpS431OcE1S47AQUwhrg8E=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
@@ -247,6 +249,7 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac h1:TSSpLIG4v+p0rPv1pNOQtl1I8knsO4S9trOxNMOLVP4=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+1 -1
View File
@@ -1 +1 @@
1.32.11
1.36.36
+24 -2
View File
@@ -72,7 +72,29 @@
"v1.36.7",
"v1.36.8",
"v1.36.9",
"v1.36.10"
"v1.36.10",
"v1.36.11",
"v1.36.12",
"v1.36.13",
"v1.36.14",
"v1.36.15",
"v1.36.16",
"v1.36.17",
"v1.36.18",
"v1.36.19",
"v1.36.20",
"v1.36.21",
"v1.36.22",
"v1.36.23",
"v1.36.24",
"v1.36.25",
"v1.36.26",
"v1.36.27",
"v1.36.28",
"v1.36.30",
"v1.36.31",
"v1.36.32",
"v1.36.33"
],
"41": [
"v1.13.2"
@@ -187,4 +209,4 @@
"v1.8.5",
"v1.8.6"
]
}
}
+1 -1
View File
@@ -77,7 +77,7 @@ var (
const (
defaultMajor = 1
defaultMinor = 36
defaultPatch = 10
defaultPatch = 35
)
func init() {
-40
View File
@@ -1,40 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package dexvm re-exports the canonical DEX VM from
// github.com/luxfi/chains/dexvm so existing callers that imported
// github.com/luxfi/node/vms/dexvm pre-extraction keep working
// without source-level changes.
//
// New code should import the canonical path:
// "github.com/luxfi/chains/dexvm"
//
// This package is a thin backward-compatibility alias. The underlying
// chains/dexvm is the pure-Go stateless atomic proxy (zero private deps).
// Unlike the always-on genesis VMs, dexvm is registered in OptionalVMs and is
// NFT-gated (see node/vms.go:118, RequiredNFT "dex-operator"): a node only
// tracks/validates the D-Chain when the network has configured that operator
// collection, so it is plugin-loaded on demand, not linked unconditionally.
package dexvm
import (
"github.com/luxfi/chains/dexvm"
)
// Re-export the public surface.
type (
Block = dexvm.Block
ChainVM = dexvm.ChainVM
Factory = dexvm.Factory
OrderKey = dexvm.OrderKey
DexVertex = dexvm.DexVertex
Status = dexvm.Status
)
var (
// VMID identifies the canonical primary-network D-Chain VM.
VMID = dexvm.VMID
// NewChainVM constructs a fresh DEX chain VM.
NewChainVM = dexvm.NewChainVM
)
-32
View File
@@ -1,32 +0,0 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package mpcvm — backend selection re-export.
//
// The canonical dlopen probe lives in github.com/luxfi/chains/mpcvm.
// Its init() runs once at package load time and pins a process-wide
// GPUBackend handle (cuda → hip → metal → vulkan → webgpu probe order).
// This file is the node-side entry point for diagnostics and ops tooling.
//
// Note: the luxd VM manager registration for ThresholdVM (M-Chain) is
// NOT flipped here per the project memory note ("M-Chain code exists but
// NOT yet registered in running luxd"). This file provides the bridge
// surface only — flipping the manager hook-up is a separate ops PR.
package mpcvm
// SelectGPUBackend returns the resolved GPU plugin (or nil) and a single
// human-readable diagnostic string. luxd startup logs use this to surface
// "mpcvm-gpu backend=<name>" lines alongside the cevm backend
// selection, matching the cevm.go pattern at
// ~/work/lux/chains/evm/backend_cgo.go.
//
// Calling this multiple times is cheap — the underlying probe runs once
// at package init via sync.Once in chains/mpcvm/backend.go.
func SelectGPUBackend() (*GPUBackend, string) {
g := GPUBackendInstance()
if g == nil || !g.IsAvailable() {
return nil, "mpcvm-gpu: no plugin resolved (CPU-only)"
}
return g, "mpcvm-gpu: backend=" + g.Kind.String() + " path=" + g.Path
}
-27
View File
@@ -1,27 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package mpcvm re-exports the canonical Threshold (FHE / MPC)
// VM from github.com/luxfi/chains/mpcvm so existing callers
// that imported github.com/luxfi/node/vms/mpcvm pre-extraction
// keep working without source changes.
//
// New code should import the canonical path:
// "github.com/luxfi/chains/mpcvm"
//
// This file is a thin alias wrapper kept for backward compatibility.
package mpcvm
import "github.com/luxfi/chains/mpcvm"
type (
Block = mpcvm.Block
BlockError = mpcvm.BlockError
Client = mpcvm.Client
Operation = mpcvm.Operation
)
var (
ErrInvalidOperation = mpcvm.ErrInvalidOperation
NewClient = mpcvm.NewClient
)
-78
View File
@@ -1,78 +0,0 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build cgo
// Package mpcvm re-exports the GPU bridge from
// github.com/luxfi/chains/mpcvm so callers that import the legacy
// node path keep working without source changes.
//
// One and only one way to dlopen: the canonical bridge lives in
// chains/mpcvm. This package is a typed alias layer — both the
// types and the GPUBackend singleton come straight from the upstream.
// There is exactly one dlopen handle, one symbol table, one init()
// probe across the entire luxd process. Consumers of either import path
// share the same plugin instance.
package mpcvm
import "github.com/luxfi/chains/mpcvm"
// =============================================================================
// Type re-exports — Go aliases preserve the public API so external callers
// (e.g. node/chains, node/main) can drop the import path without source
// changes. The GPU- prefix avoids collisions with the domain types
// already aliased above (Block, Client, Operation).
// =============================================================================
type (
GPUBackend = mpcvm.GPUBackend
GPUBackendKind = mpcvm.GPUBackendKind
GPUCeremony = mpcvm.GPUCeremony
GPUKeyShare = mpcvm.GPUKeyShare
GPUContribution = mpcvm.GPUContribution
GPUMPCVMState = mpcvm.GPUMPCVMState
GPUMPCVMRoundDescriptor = mpcvm.GPUMPCVMRoundDescriptor
GPUCeremonyOp = mpcvm.GPUCeremonyOp
GPUContributionOp = mpcvm.GPUContributionOp
GPUMPCVMTransitionResult = mpcvm.GPUMPCVMTransitionResult
)
// Backend constants re-exported. Matches the chains/mpcvm dlopen
// probe order: cuda → hip → metal → vulkan → webgpu.
const (
GPUBackendNone = mpcvm.GPUBackendNone
GPUBackendCUDA = mpcvm.GPUBackendCUDA
GPUBackendHIP = mpcvm.GPUBackendHIP
GPUBackendMetal = mpcvm.GPUBackendMetal
GPUBackendVulkan = mpcvm.GPUBackendVulkan
GPUBackendWebGPU = mpcvm.GPUBackendWebGPU
)
// ErrGPUNotAvailable is the canonical "no plugin loaded" error. Callers
// `errors.Is(err, mpcvm.ErrGPUNotAvailable)` to distinguish a
// fallback-to-CPU condition from a hard launcher failure.
var ErrGPUNotAvailable = mpcvm.ErrGPUNotAvailable
// GPUBackendInstance returns the dlopen'd GPU plugin handle resolved at
// chains/mpcvm package init. nil means no plugin was loaded
// (CPU-only mode). The handle is shared across the whole process — both
// the node and chains paths see the same instance.
//
// The name is GPUBackendInstance (not GPUBackend / Backend) because Go
// disallows a function named the same as a type alias in the same
// package, and `Backend` is already a domain term in the threshold
// state machine. Callers write:
//
// if g := mpcvm.GPUBackendInstance(); g != nil && g.IsAvailable() {
// _, err := g.CeremonyApply(desc, ops, ceremonies)
// ...
// }
//
// This mirrors the cevm pattern `cevm.AvailableBackends()` /
// `cevm.LibraryABIVersion()` — discovery via package-scope function,
// not via a global variable.
func GPUBackendInstance() *GPUBackend {
return mpcvm.Backend()
}
-49
View File
@@ -1,49 +0,0 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build !cgo
// nocgo re-export of the chains/mpcvm GPU bridge. Under !cgo the
// upstream GPUBackend methods all return ErrGPUNotAvailable and
// Backend() returns nil; this layer transparently passes that through
// so callers see identical behaviour regardless of build mode.
//
// One and only one nocgo stub for the entire process: it lives in
// chains/mpcvm. This package just re-exports the types and the
// sentinel error.
package mpcvm
import "github.com/luxfi/chains/mpcvm"
type (
GPUBackend = mpcvm.GPUBackend
GPUBackendKind = mpcvm.GPUBackendKind
GPUCeremony = mpcvm.GPUCeremony
GPUKeyShare = mpcvm.GPUKeyShare
GPUContribution = mpcvm.GPUContribution
GPUMPCVMState = mpcvm.GPUMPCVMState
GPUMPCVMRoundDescriptor = mpcvm.GPUMPCVMRoundDescriptor
GPUCeremonyOp = mpcvm.GPUCeremonyOp
GPUContributionOp = mpcvm.GPUContributionOp
GPUMPCVMTransitionResult = mpcvm.GPUMPCVMTransitionResult
)
const (
GPUBackendNone = mpcvm.GPUBackendNone
GPUBackendCUDA = mpcvm.GPUBackendCUDA
GPUBackendHIP = mpcvm.GPUBackendHIP
GPUBackendMetal = mpcvm.GPUBackendMetal
GPUBackendVulkan = mpcvm.GPUBackendVulkan
GPUBackendWebGPU = mpcvm.GPUBackendWebGPU
)
var ErrGPUNotAvailable = mpcvm.ErrGPUNotAvailable
// GPUBackendInstance returns nil under !cgo — the upstream Backend()
// returns nil because no dlopen ever happens. Callers branch on the
// IsAvailable() check (or `g == nil`) and route to the CPU reference.
func GPUBackendInstance() *GPUBackend {
return mpcvm.Backend()
}
-194
View File
@@ -1,194 +0,0 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package mpcvm
// TestGPUBridgeCgoNocgoParity is the node-side mirror of the parity
// test in chains/mpcvm — proves that the re-exported GPUBackend
// method set produces byte-identical MPC ceremony state transitions
// regardless of build flavor (cgo / nocgo) and plugin presence.
//
// The node package is a thin alias layer over chains/mpcvm —
// type aliases on the wire structs and a re-exported GPUBackendInstance
// accessor — so the parity properties of the canonical bridge transfer
// directly. We re-run the four-op pipeline through the node-side
// surface to pin that the alias layer doesn't drop any state on the
// floor.
import (
"strings"
"testing"
chainsthreshold "github.com/luxfi/chains/mpcvm"
)
// TestGPUBridgeCgoNocgoParity runs a 3-of-5 FROST keygen ceremony
// through GPUBackendInstance() (the node-side accessor) and compares
// the resulting arena byte-for-byte to a fresh run via the canonical
// bridge in chains/mpcvm. Under cgo, the comparison is the
// upstream cgo bridge vs itself (both runs hit the same dispatcher).
// Under !cgo, the comparison is the upstream nocgo bridge vs itself.
// Either path proves the alias layer is transparent and the substrate
// transitions deterministically.
func TestGPUBridgeCgoNocgoParity(t *testing.T) {
const cid uint64 = 0xCEFA01CA001
const N = 16 // power of 2 for the open-addressing locator
var subject, seed [32]byte
for i := 0; i < 32; i++ {
subject[i] = byte(i + 1)
seed[i] = byte(0xA0 ^ i)
}
beginOps := []chainsthreshold.GPUCeremonyOp{
{
CeremonyID: cid,
DeadlineNs: 10_000_000_000,
Kind: 0, // kCeremonyOpBegin
CeremonyKind: 0, // kKindFrostKeygen → 3 rounds
Threshold: 3,
TotalParticipants: 5,
Subject: subject,
CeremonySeed: seed,
},
}
buildRound := func(round uint32) []chainsthreshold.GPUContributionOp {
ops := make([]chainsthreshold.GPUContributionOp, 5)
for h := uint32(0); h < 5; h++ {
var payload [384]byte
for k := 0; k < 16; k++ {
payload[k] = byte((round * 16) + h*4 + uint32(k))
}
ops[h] = chainsthreshold.GPUContributionOp{
CeremonyID: cid,
HolderAddr: uint64(0xDEAD0000 | h),
Round: round,
HolderIndex: h,
PayloadLen: 16,
Payload: payload,
}
}
return ops
}
r1 := buildRound(0)
r2 := buildRound(1)
r3 := buildRound(2)
desc1 := &GPUMPCVMRoundDescriptor{
ChainID: 0xABBA,
Round: 1,
TimestampNs: 1_000_000_000,
Epoch: 1,
CeremonyOpCount: 1,
ContributionOpCount: 5,
}
desc2 := &GPUMPCVMRoundDescriptor{
ChainID: 0xABBA,
Round: 2,
TimestampNs: 2_000_000_000,
Epoch: 1,
ContributionOpCount: 5,
}
desc3 := &GPUMPCVMRoundDescriptor{
ChainID: 0xABBA,
Round: 3,
TimestampNs: 3_000_000_000,
Epoch: 1,
ContributionOpCount: 5,
}
descClose := &GPUMPCVMRoundDescriptor{
ChainID: 0xABBA,
Round: 4,
TimestampNs: 4_000_000_000,
Epoch: 1,
ClosingFlag: 1,
}
// Run via node-side re-export.
runVia := func(t *testing.T) (
[]GPUCeremony,
[]GPUKeyShare,
[]GPUContribution,
GPUMPCVMState,
GPUMPCVMTransitionResult,
) {
t.Helper()
cer := make([]GPUCeremony, N)
keys := make([]GPUKeyShare, N)
con := make([]GPUContribution, N)
b := GPUBackendInstance() // nil-receiver-safe via fallback to CPU reference
if _, err := b.CeremonyApply(desc1, beginOps, cer); err != nil {
if strings.Contains(err.Error(), "GPU backend not available") {
t.Skip("GPU plugin not dlopened in this build; CPU-only path covered elsewhere")
}
t.Fatalf("CeremonyApply r0: %v", err)
}
if _, err := b.ContributionApply(desc1, r1, cer, con, 1); err != nil {
t.Fatalf("ContributionApply r0: %v", err)
}
if _, _, _, err := b.KeyShareApply(desc1, cer, keys, con, 1); err != nil {
t.Fatalf("KeyShareApply r0: %v", err)
}
if _, err := b.ContributionApply(desc2, r2, cer, con, 6); err != nil {
t.Fatalf("ContributionApply r1: %v", err)
}
if _, _, _, err := b.KeyShareApply(desc2, cer, keys, con, 1); err != nil {
t.Fatalf("KeyShareApply r1: %v", err)
}
if _, err := b.ContributionApply(desc3, r3, cer, con, 11); err != nil {
t.Fatalf("ContributionApply r2: %v", err)
}
if _, _, _, err := b.KeyShareApply(desc3, cer, keys, con, 1); err != nil {
t.Fatalf("KeyShareApply r2: %v", err)
}
var state GPUMPCVMState
res, err := b.MPCTransition(descClose, cer, keys, con, &state)
if err != nil {
t.Fatalf("MPCTransition: %v", err)
}
return cer, keys, con, state, *res
}
cerA, keysA, conA, stateA, resA := runVia(t)
cerB, keysB, conB, stateB, resB := runVia(t)
for i := range cerA {
if cerA[i] != cerB[i] {
t.Errorf("ceremony slot %d differs across runs:\nA=%+v\nB=%+v", i, cerA[i], cerB[i])
}
}
for i := range keysA {
if keysA[i] != keysB[i] {
t.Errorf("keyShare slot %d differs across runs:\nA=%+v\nB=%+v", i, keysA[i], keysB[i])
}
}
for i := range conA {
if conA[i] != conB[i] {
t.Errorf("contribution slot %d differs across runs:\nA=%+v\nB=%+v", i, conA[i], conB[i])
}
}
if stateA != stateB {
t.Errorf("state differs across runs:\nA=%+v\nB=%+v", stateA, stateB)
}
if resA != resB {
t.Errorf("result differs across runs:\nA=%+v\nB=%+v", resA, resB)
}
// Sanity — the ceremony MUST have finalized.
if stateA.FinalizedCeremonyCount != 1 {
t.Errorf("expected 1 finalized ceremony, got %d", stateA.FinalizedCeremonyCount)
}
if stateA.KeyShareCount != 5 {
t.Errorf("expected 5 emitted key shares, got %d", stateA.KeyShareCount)
}
var zero [32]byte
if stateA.MPCVMStateRoot == zero {
t.Errorf("mpcvm_state_root is zero — the fold produced no output")
}
}
-107
View File
@@ -1,107 +0,0 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package mpcvm
import (
"errors"
"testing"
"unsafe"
)
// TestNodeGPULayoutSizes pins the re-exported wire structs to the same
// device-side __align__(16) values declared in
// the GPU plugin install tree ops/mpcvm/cuda/mpcvm_kernels_common.cuh.
//
// Even though the structs are type aliases to chains/mpcvm, this
// test sits at the node import boundary — if upstream sizes drift the
// node-side surface MUST also fail loud rather than miscompile.
func TestNodeGPULayoutSizes(t *testing.T) {
cases := []struct {
name string
want uintptr
got uintptr
}{
{"GPUCeremony", 128, unsafe.Sizeof(GPUCeremony{})},
{"GPUKeyShare", 368, unsafe.Sizeof(GPUKeyShare{})},
{"GPUContribution", 432, unsafe.Sizeof(GPUContribution{})},
{"GPUMPCVMState", 160, unsafe.Sizeof(GPUMPCVMState{})},
{"GPUMPCVMRoundDescriptor", 96, unsafe.Sizeof(GPUMPCVMRoundDescriptor{})},
{"GPUCeremonyOp", 96, unsafe.Sizeof(GPUCeremonyOp{})},
{"GPUContributionOp", 416, unsafe.Sizeof(GPUContributionOp{})},
{"GPUMPCVMTransitionResult", 176, unsafe.Sizeof(GPUMPCVMTransitionResult{})},
}
for _, c := range cases {
if c.got != c.want {
t.Errorf("%s: sizeof=%d want=%d", c.name, c.got, c.want)
}
}
}
// TestNodeGPUBackendRoundTrip is the dlopen round-trip required by the
// task spec. Two acceptable outcomes:
//
// 1. Plugin resolved: GPUBackendInstance() != nil && IsAvailable(); a
// zero-fixture CeremonyApply against the best backend returns rc=0
// (no ops processed) with no error.
//
// 2. Plugin absent: GPUBackendInstance() == nil; nil-receiver methods
// return ErrGPUNotAvailable. SelectGPUBackend reports the CPU-only
// diagnostic string.
//
// Both outcomes count as passing — the bridge correctly surfaces GPU
// state through the public node API.
func TestNodeGPUBackendRoundTrip(t *testing.T) {
b, diag := SelectGPUBackend()
t.Logf("SelectGPUBackend: %s", diag)
if b == nil {
// Plugin-absent path. The nil receiver contract must hold —
// every method returns ErrGPUNotAvailable, no panic.
_, err := (*GPUBackend)(nil).CeremonyApply(nil, nil, nil)
if !errors.Is(err, ErrGPUNotAvailable) {
t.Fatalf("nil receiver CeremonyApply: want ErrGPUNotAvailable, got %v", err)
}
return
}
// Plugin-resolved path. Zero fixture: 1-slot ceremony arena, no ops.
// The kernel walks zero ops and returns applied=0 with rc=0. Any
// non-zero rc means a real launcher failure.
desc := &GPUMPCVMRoundDescriptor{
ChainID: 0xCAFEBABE,
Round: 1,
TimestampNs: 1700000000_000000000,
Epoch: 1,
}
ceremonies := make([]GPUCeremony, 1)
applied, err := b.CeremonyApply(desc, nil, ceremonies)
if err != nil {
t.Fatalf("CeremonyApply(zero): %v", err)
}
if applied != 0 {
t.Errorf("CeremonyApply(zero): applied=%d want=0", applied)
}
}
// TestNodeGPUStubContract verifies the nocgo-equivalent contract on a
// zero-value GPUBackend: IsAvailable()==false and every state-machine
// method returns ErrGPUNotAvailable. The same contract holds under cgo
// when no plugin is dlopened — making this test build-flavor-independent.
func TestNodeGPUStubContract(t *testing.T) {
var b GPUBackend
if b.IsAvailable() {
t.Fatal("zero GPUBackend.IsAvailable() must be false")
}
if _, err := b.CeremonyApply(nil, nil, nil); !errors.Is(err, ErrGPUNotAvailable) {
t.Errorf("CeremonyApply: want ErrGPUNotAvailable, got %v", err)
}
if _, _, _, err := b.KeyShareApply(nil, nil, nil, nil, 0); !errors.Is(err, ErrGPUNotAvailable) {
t.Errorf("KeyShareApply: want ErrGPUNotAvailable, got %v", err)
}
if _, err := b.ContributionApply(nil, nil, nil, nil, 0); !errors.Is(err, ErrGPUNotAvailable) {
t.Errorf("ContributionApply: want ErrGPUNotAvailable, got %v", err)
}
if _, err := b.MPCTransition(nil, nil, nil, nil, nil); !errors.Is(err, ErrGPUNotAvailable) {
t.Errorf("MPCTransition: want ErrGPUNotAvailable, got %v", err)
}
}
+14
View File
@@ -87,10 +87,24 @@ func (b *Block) Verify(ctx context.Context) error {
}
func (b *Block) Accept(context.Context) error {
// Serialize the block's state commit (acceptor → state.Apply/CommitBatch →
// state.write) with the VM's OTHER writers of the same shared state — the
// peer Disconnect and Start/StopTracking uptime flushes, which run on
// goroutines the consensus engine does not serialize against accept (it
// invokes VM.Accept as a lock-free call-out). Holding the VM's stateLock for
// the whole visit makes accept atomic w.r.t. those commits, closing the
// concurrent-map-write in state.write(). Mirrors avalanchego's ctx.Lock,
// which serializes block accept with engine.Connected/Disconnected.
b.manager.stateLock.Lock()
defer b.manager.stateLock.Unlock()
return b.Visit(b.manager.acceptor)
}
func (b *Block) Reject(context.Context) error {
// Held under the same lock as Accept so a block DECISION (accept or reject)
// is uniformly serialized with the VM's peer-lifecycle state commits.
b.manager.stateLock.Lock()
defer b.manager.stateLock.Unlock()
return b.Visit(b.manager.rejector)
}
+16 -2
View File
@@ -7,8 +7,8 @@ import (
"context"
"errors"
"fmt"
"sync"
"github.com/luxfi/vm/chain"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/math/set"
@@ -20,6 +20,7 @@ import (
"github.com/luxfi/node/vms/platformvm/txs/fee"
"github.com/luxfi/node/vms/platformvm/validators"
"github.com/luxfi/node/vms/txs/mempool"
"github.com/luxfi/vm/chain"
)
var (
@@ -57,6 +58,15 @@ func NewManager(
s state.State,
txExecutorBackend *executor.Backend,
validatorManager validators.Manager,
// stateLock serializes a block's accept/reject state commit with the VM's
// OTHER writers of the same shared state — the peer-lifecycle (Disconnect)
// and normal-ops (Start/StopTracking) uptime flushes, which run on
// goroutines the consensus engine does not serialize. The platform VM owns
// it and holds the SAME lock in those paths; without it a peer disconnect's
// state.Commit races a block accept's state.CommitBatch inside state.write()
// (concurrent Go map writes → fatal). Must be non-nil (the VM always
// supplies &vm.stateLock).
stateLock *sync.Mutex,
) Manager {
lastAccepted := s.GetLastAccepted()
backend := &backend{
@@ -81,6 +91,7 @@ func NewManager(
preferred: lastAccepted,
txExecutorBackend: txExecutorBackend,
validatorManager: validatorManager,
stateLock: stateLock,
Log: log.Noop(),
}
}
@@ -93,7 +104,10 @@ type manager struct {
preferred ids.ID
txExecutorBackend *executor.Backend
validatorManager validators.Manager
Log log.Logger
// stateLock serializes block accept/reject (Block.Accept/Reject) with the
// VM's peer-lifecycle / normal-ops state commits. See NewManager.
stateLock *sync.Mutex
Log log.Logger
}
func (m *manager) GetBlock(blkID ids.ID) (chain.Block, error) {
+5 -4
View File
@@ -926,11 +926,12 @@ func (s *Service) getPrimaryOrNetValidators(netID ids.ID, nodeIDs set.Set[ids.No
// Transform this to a percentage (0-100) to make it consistent
// with observedUptime in info.peers API
currentUptime := avajson.Float32(rawUptime * 100)
if err != nil {
return nil, err
}
// connected field left nil - IsConnected method no longer exists
uptime = &currentUptime
// Report whether this validator currently has a live connection
// to us, read from the same tracker that measured its uptime.
isConnected := s.vm.tracker != nil && s.vm.tracker.IsConnected(currentStaker.NodeID)
connected = &isConnected
}
var (
+150
View File
@@ -0,0 +1,150 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package platformvm
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/luxfi/constants"
"github.com/luxfi/database/memdb"
"github.com/luxfi/log"
"github.com/luxfi/metric"
"github.com/luxfi/runtime"
validators "github.com/luxfi/validators"
"github.com/luxfi/node/upgrade"
"github.com/luxfi/node/vms/platformvm/config"
"github.com/luxfi/node/vms/platformvm/genesis/genesistest"
platformvmmetrics "github.com/luxfi/node/vms/platformvm/metrics"
"github.com/luxfi/node/vms/platformvm/reward"
"github.com/luxfi/node/vms/platformvm/state"
)
// newRaceTestState builds a REAL platform state (state.New, memdb + genesis) —
// the same constructor the VM uses. The genesis carries a validator set, so
// state.write (invoked by BOTH state.Commit and state.CommitBatch) iterates
// non-empty validator/metadata maps: exactly the shared maps that mutate
// concurrently in the fatal this test guards against.
func newRaceTestState(t *testing.T) state.State {
t.Helper()
require := require.New(t)
reg := metric.NewRegistry()
m, err := platformvmmetrics.New(reg)
require.NoError(err)
execCfg, err := config.GetConfig(nil)
require.NoError(err)
st, err := state.New(
memdb.New(),
genesistest.NewBytes(t, genesistest.Config{}),
reg,
validators.NewManager(),
upgrade.GetConfig(constants.UnitTestID),
execCfg,
&runtime.Runtime{
NetworkID: constants.UnitTestID,
ChainID: constants.PlatformChainID,
Log: log.Noop(),
},
m,
reward.NewCalculator(reward.Config{
MaxConsumptionRate: 120_000,
MinConsumptionRate: 100_000,
MintingPeriod: 365 * 24 * time.Hour,
SupplyCap: 720 * constants.MegaLux,
}),
)
require.NoError(err)
return st
}
// TestStateCommitSerializedWithAcceptNoRace is the regression guard for RED
// CRITICAL #2 (P-chain state race).
//
// Before the fix the event-delivery plumbing drove VM.Disconnected on the
// node's peer-lifecycle goroutine, where it called state.Commit() (→ state.write)
// concurrently with the block acceptor's state.CommitBatch() (→ state.write) on
// the consensus accept goroutine. Both mutate the shared currentValidator /
// staker / metadata maps with no common lock → Go "concurrent map writes" FATAL
// on ordinary peer churn.
//
// The fix installs ONE lock — the platform VM's stateLock — held by BOTH sides:
// - block DECISION: block/executor Block.Accept/Reject hold &vm.stateLock
// (supplied to executor.NewManager), around the whole acceptor visit, and
// - peer/lifecycle commits: VM.Disconnected and the Start/StopTracking uptime
// flushes hold vm.stateLock directly.
//
// This test drives the two REAL racing state operations — state.Commit()
// (VM.Disconnected's exact call) and state.SetUptime()+state.CommitBatch()
// (the tracker-flush + acceptor's exact calls) — from two goroutines, each
// holding the SAME lock the fix installs. Under `-race` it must complete with
// no data race and no fatal. Remove the shared lock and `-race` immediately
// reports the concurrent write inside state.write() (the regression).
func TestStateCommitSerializedWithAcceptNoRace(t *testing.T) {
st := newRaceTestState(t)
// The single serializer the fix installs. In production this is
// &vm.stateLock, held by Block.Accept (via the executor manager) and by
// VM.Disconnected/Start/StopTracking.
var stateLock sync.Mutex
// A genesis validator so SetUptime writes a real record (mirrors
// tracker.Disconnect → state.SetUptime before state.Commit).
nodeID := genesistest.DefaultNodeIDs[0]
netID := constants.PrimaryNetworkID
const iterations = 300
var (
wg sync.WaitGroup
acceptErr error
disconnectErr error
)
wg.Add(2)
// Accept side: the acceptor's state.CommitBatch() + state.Abort() — the exact
// calls block/executor acceptor.standardBlock makes, both routed through
// state.write(). Block.Accept holds stateLock around this.
go func() {
defer wg.Done()
for i := 0; i < iterations; i++ {
stateLock.Lock()
_, err := st.CommitBatch()
st.Abort()
stateLock.Unlock()
if err != nil {
acceptErr = err
return
}
}
}()
// Disconnect side: tracker.Disconnect (state.SetUptime, error ignored exactly
// as the tracker's updateUptimeLocked ignores ErrNotFound) followed by
// state.Commit() — VM.Disconnected's exact sequence. Holds the SAME stateLock.
go func() {
defer wg.Done()
for i := 0; i < iterations; i++ {
stateLock.Lock()
_ = st.SetUptime(nodeID, netID, time.Duration(i)*time.Second, time.Unix(int64(i), 0))
err := st.Commit()
stateLock.Unlock()
if err != nil {
disconnectErr = err
return
}
}
}()
wg.Wait()
require := require.New(t)
require.NoError(acceptErr)
require.NoError(disconnectErr)
}
+218 -114
View File
@@ -4,91 +4,230 @@
package platformvm
import (
"errors"
"sync"
"time"
"github.com/luxfi/database"
"github.com/luxfi/ids"
"github.com/luxfi/validators/uptime"
)
// uptimeTracker implements uptime.Calculator by tracking peer connections
// and computing real uptime percentages from persistent state.
var (
errAlreadyStartedTracking = errors.New("uptime tracker already started tracking")
errNotStartedTracking = errors.New("uptime tracker has not started tracking")
)
// uptimeTracker implements uptime.Calculator plus the connection/tracking hooks
// the platform VM drives directly (Connect, Disconnect, StartTracking,
// StopTracking, IsConnected).
//
// It wraps an uptime.State (backed by platformvm state) that stores
// cumulative upDuration and lastUpdated per validator. On Connect, it
// records the connection time. On Disconnect, it flushes the elapsed
// connected time into the persistent state. CalculateUptimePercent
// reads from state and accounts for any currently-connected time.
// It is a faithful port of avalanchego's snow/uptime.Manager, adapted to the
// luxfi validators uptime.State interface (which keys by netID and returns
// lastUpdated encoded as a second-granular duration since the Unix epoch).
//
// Model, in one sentence: each connected peer's session accrues into a per
// validator up-duration that is folded forward to "now" on read, and persisted
// on flush.
//
// - Connect records a peer's connection time. It is captured from the very
// first handshake — including during bootstrap, before StartTracking — so a
// stable, continuously-connected validator set is fully observed the moment
// tracking begins.
// - StartTracking, called once at P-chain normal-operations start, baselines
// every validator (crediting the pre-tracking window as online, matching
// avalanchego) and switches into live-tracking mode.
// - CalculateUptime folds the currently-connected session forward to now, so a
// validator that stays connected accrues up-duration WITHOUT ever needing a
// Disconnect.
// - Disconnect / StopTracking flush the accrued session into persistent state.
//
// The prior custom tracker could not accrue uptime for a stable set: it never
// started tracking, only flushed on Disconnect, and — because peer connection
// events were never delivered to the VM — never populated its connected map.
type uptimeTracker struct {
mu sync.RWMutex
clk func() time.Time
state uptime.State
netID ids.ID
connected map[ids.NodeID]time.Time // nodeID -> time they connected
mu sync.RWMutex
clk func() time.Time
state uptime.State
netID ids.ID
// connections maps a currently-connected nodeID to the (second-granular)
// time it connected.
connections map[ids.NodeID]time.Time
// startedTracking gates live-session accounting. Before StartTracking, a
// validator is assumed online since its last persisted update (so the
// bootstrap window is not spuriously counted as downtime). After
// StartTracking, only genuinely-connected sessions accrue.
startedTracking bool
}
func newUptimeTracker(state uptime.State, netID ids.ID, clk func() time.Time) *uptimeTracker {
return &uptimeTracker{
clk: clk,
state: state,
netID: netID,
connected: make(map[ids.NodeID]time.Time),
clk: clk,
state: state,
netID: netID,
connections: make(map[ids.NodeID]time.Time),
}
}
// Connect records that a validator connected.
// now returns the tracker clock truncated to second precision. lastUpdated is
// persisted at second granularity (state stores lastUpdated.Unix()), so working
// at one resolution keeps the interval arithmetic exact and side-steps
// monotonic-clock skew.
func (t *uptimeTracker) now() time.Time {
return time.Unix(t.clk().Unix(), 0)
}
// lastUpdatedFromDuration reconstructs the persisted lastUpdated timestamp from
// the second-granular "duration since epoch" the uptime.State returns.
func lastUpdatedFromDuration(d time.Duration) time.Time {
return time.Unix(int64(d/time.Second), 0)
}
// Connect records that [nodeID] connected. It is idempotent: a duplicate Connect
// for an already-connected node keeps the original connection time, so repeated
// router dispatch of the same live connection can never reset the session clock.
func (t *uptimeTracker) Connect(nodeID ids.NodeID) {
t.mu.Lock()
defer t.mu.Unlock()
if _, ok := t.connected[nodeID]; ok {
return // already connected
if _, ok := t.connections[nodeID]; ok {
return
}
t.connected[nodeID] = t.clk()
t.connections[nodeID] = t.now()
}
// Disconnect records that a validator disconnected and flushes
// the accumulated uptime into persistent state.
func (t *uptimeTracker) Disconnect(nodeID ids.NodeID) error {
// IsConnected reports whether [nodeID] currently has a live connection.
func (t *uptimeTracker) IsConnected(nodeID ids.NodeID) bool {
t.mu.RLock()
defer t.mu.RUnlock()
_, connected := t.connections[nodeID]
return connected
}
// Disconnect records that [nodeID] disconnected, flushing its accrued session
// into persistent state. It returns whether that flush actually wrote uptime
// state (a SetUptime the caller must Commit). Flushing is a no-op — mutated
// false — before StartTracking (the session is not yet being measured, matching
// avalanchego) and for a peer with no uptime record (a non-validator), so the
// caller can skip an empty state.Commit.
func (t *uptimeTracker) Disconnect(nodeID ids.NodeID) (mutated bool, err error) {
t.mu.Lock()
defer t.mu.Unlock()
return t.disconnectLocked(nodeID)
}
func (t *uptimeTracker) disconnectLocked(nodeID ids.NodeID) error {
connectedAt, ok := t.connected[nodeID]
if !ok {
return nil // wasn't connected
defer delete(t.connections, nodeID)
if !t.startedTracking {
return false, nil
}
delete(t.connected, nodeID)
now := t.clk()
elapsed := now.Sub(connectedAt)
return t.addUptime(nodeID, elapsed, now)
return t.updateUptimeLocked(nodeID)
}
// addUptime adds elapsed duration to the validator's persistent uptime.
func (t *uptimeTracker) addUptime(nodeID ids.NodeID, elapsed time.Duration, now time.Time) error {
upDuration, _, err := t.state.GetUptime(nodeID, t.netID)
if err != nil {
// Validator not in state (e.g., not a current validator). Skip.
return nil
}
return t.state.SetUptime(nodeID, t.netID, upDuration+elapsed, now)
}
// Shutdown flushes all connected validators' uptime to state.
// Call this before persisting state on node shutdown.
func (t *uptimeTracker) Shutdown() error {
// StartTracking baselines every validator in [nodeIDs] and switches into live
// tracking mode. It is called once at P-chain normal-operations start. Each
// validator's persisted up-duration is advanced to now assuming it was online
// since its last update (the standard avalanchego assumption), so a validator
// that has been in the set is not penalized for the un-measured pre-tracking
// window.
func (t *uptimeTracker) StartTracking(nodeIDs []ids.NodeID) error {
t.mu.Lock()
defer t.mu.Unlock()
for nodeID := range t.connected {
if err := t.disconnectLocked(nodeID); err != nil {
if t.startedTracking {
return errAlreadyStartedTracking
}
for _, nodeID := range nodeIDs {
if _, err := t.updateUptimeLocked(nodeID); err != nil {
return err
}
}
t.startedTracking = true
return nil
}
// CalculateUptime returns (upDuration, totalDuration, error) for a validator.
// StopTracking flushes every validator in [nodeIDs] and leaves tracking mode.
// It is called on the normal-ops → bootstrapping / shutdown transition so the
// connected sessions are durably persisted before the node stops measuring.
func (t *uptimeTracker) StopTracking(nodeIDs []ids.NodeID) error {
t.mu.Lock()
defer t.mu.Unlock()
if !t.startedTracking {
return errNotStartedTracking
}
for _, nodeID := range nodeIDs {
if _, err := t.updateUptimeLocked(nodeID); err != nil {
return err
}
}
t.startedTracking = false
return nil
}
// StartedTracking reports whether StartTracking has run and StopTracking has not
// since. The VM lifecycle uses it to avoid double-start and to gate shutdown
// flushing.
func (t *uptimeTracker) StartedTracking() bool {
t.mu.RLock()
defer t.mu.RUnlock()
return t.startedTracking
}
// calculateUptimeLocked mirrors avalanchego snow/uptime.Manager.CalculateUptime:
// it returns [nodeID]'s up-duration folded forward to now, plus that now (the
// new lastUpdated). The caller must hold t.mu (read or write).
func (t *uptimeTracker) calculateUptimeLocked(nodeID ids.NodeID) (time.Duration, time.Time, error) {
upDuration, lastUpdatedDur, err := t.state.GetUptime(nodeID, t.netID)
if err != nil {
return 0, time.Time{}, err
}
lastUpdated := lastUpdatedFromDuration(lastUpdatedDur)
now := t.now()
// Clock skew: never subtract time or double-count.
if now.Before(lastUpdated) {
return upDuration, lastUpdated, nil
}
// Before tracking, assume the node was online since its last update.
if !t.startedTracking {
return upDuration + now.Sub(lastUpdated), now, nil
}
// Tracking, but not connected: offline since its last update.
connectedAt, isConnected := t.connections[nodeID]
if !isConnected {
return upDuration, now, nil
}
// Tracking and connected: credit from the later of (connect, lastUpdated) so
// no interval is double-counted.
if connectedAt.Before(lastUpdated) {
connectedAt = lastUpdated
}
if now.Before(connectedAt) {
return upDuration, now, nil
}
return upDuration + now.Sub(connectedAt), now, nil
}
// updateUptimeLocked persists the current up-duration for [nodeID], returning
// whether it actually wrote (mutated). A node without a state record (i.e. not a
// current validator) is silently skipped — mutated false — so tracking
// non-validator peers is harmless and never dirties the state. The caller must
// hold t.mu (write).
func (t *uptimeTracker) updateUptimeLocked(nodeID ids.NodeID) (mutated bool, err error) {
upDuration, lastUpdated, err := t.calculateUptimeLocked(nodeID)
if errors.Is(err, database.ErrNotFound) {
return false, nil
}
if err != nil {
return false, err
}
if err := t.state.SetUptime(nodeID, t.netID, upDuration, lastUpdated); err != nil {
return false, err
}
return true, nil
}
// CalculateUptime returns (upDuration, totalDuration) for [nodeID], where
// totalDuration is the maximum possible uptime since the validator's start.
func (t *uptimeTracker) CalculateUptime(nodeID ids.NodeID, netID ids.ID) (time.Duration, time.Duration, error) {
if netID != t.netID {
return 0, 0, nil
@@ -99,97 +238,62 @@ func (t *uptimeTracker) CalculateUptime(nodeID ids.NodeID, netID ids.ID) (time.D
return 0, 0, err
}
now := t.clk()
totalDuration := now.Sub(startTime)
if totalDuration <= 0 {
return 0, 0, nil
}
upDuration, _, err := t.state.GetUptime(nodeID, netID)
if err != nil {
return 0, totalDuration, nil
}
// Add any currently-connected time that hasn't been flushed yet.
t.mu.RLock()
if connectedAt, ok := t.connected[nodeID]; ok {
upDuration += now.Sub(connectedAt)
}
upDuration, _, err := t.calculateUptimeLocked(nodeID)
t.mu.RUnlock()
if upDuration > totalDuration {
upDuration = totalDuration
if err != nil {
return 0, 0, err
}
return upDuration, totalDuration, nil
total := t.now().Sub(startTime)
if total < 0 {
total = 0
}
return upDuration, total, nil
}
// CalculateUptimePercent returns the uptime as a fraction in [0, 1].
// CalculateUptimePercent returns [nodeID]'s uptime over its whole staking period
// as a fraction in [0, 1].
func (t *uptimeTracker) CalculateUptimePercent(nodeID ids.NodeID, netID ids.ID) (float64, error) {
if netID != t.netID {
return 0, nil
}
upDuration, totalDuration, err := t.CalculateUptime(nodeID, netID)
startTime, err := t.state.GetStartTime(nodeID, netID)
if err != nil {
return 0, err
}
if totalDuration == 0 {
return 1, nil // no time elapsed, consider 100%
}
return float64(upDuration) / float64(totalDuration), nil
return t.CalculateUptimePercentFrom(nodeID, netID, startTime)
}
// CalculateUptimePercentFrom returns the uptime as a fraction since [from].
// CalculateUptimePercentFrom returns [nodeID]'s uptime since [from] as a fraction
// in [0, 1].
func (t *uptimeTracker) CalculateUptimePercentFrom(nodeID ids.NodeID, netID ids.ID, from time.Time) (float64, error) {
if netID != t.netID {
return 0, nil
}
now := t.clk()
totalDuration := now.Sub(from)
if totalDuration <= 0 {
return 1, nil
}
upDuration, _, err := t.state.GetUptime(nodeID, netID)
if err != nil {
return 0, nil
}
// Subtract uptime before [from] by using startTime.
// If from > startTime, some of the stored upDuration may predate [from].
// We approximate by assuming the same uptime rate.
startTime, err := t.state.GetStartTime(nodeID, netID)
if err != nil {
return 0, nil
}
totalSinceStart := now.Sub(startTime)
if totalSinceStart <= 0 {
return 1, nil
}
// Add any currently-connected time.
t.mu.RLock()
if connectedAt, ok := t.connected[nodeID]; ok {
upDuration += now.Sub(connectedAt)
}
upDuration, _, err := t.calculateUptimeLocked(nodeID)
t.mu.RUnlock()
if upDuration > totalSinceStart {
upDuration = totalSinceStart
if err != nil {
return 0, err
}
// Scale upDuration to the [from, now] window.
if from.After(startTime) {
rate := float64(upDuration) / float64(totalSinceStart)
return rate, nil
bestPossible := t.now().Sub(from)
if bestPossible <= 0 {
return 1, nil
}
// from <= startTime, just use overall rate.
return float64(upDuration) / float64(totalSinceStart), nil
fraction := float64(upDuration) / float64(bestPossible)
if fraction > 1 {
fraction = 1
}
return fraction, nil
}
// SetCalculator is a no-op; this tracker doesn't delegate.
// SetCalculator is a no-op: this tracker is a concrete Calculator and does not
// delegate. It exists only to satisfy uptime.Calculator; registration is done by
// the enclosing uptime.LockedCalculator.
func (t *uptimeTracker) SetCalculator(ids.ID, uptime.Calculator) error {
return nil
}
+288 -259
View File
@@ -8,11 +8,17 @@ import (
"testing"
"time"
"github.com/luxfi/ids"
"github.com/stretchr/testify/require"
"github.com/luxfi/database"
"github.com/luxfi/ids"
)
// fakeUptimeState implements uptime.State for testing.
// fakeUptimeState implements uptime.State for testing, mirroring how the real
// platformvm state stores uptime: an up-duration plus a second-granular
// lastUpdated, returned as a "duration since the Unix epoch". Validators must be
// registered first; GetUptime/SetUptime on an unregistered node return
// database.ErrNotFound, exactly like metadata_validator.go.
type fakeUptimeState struct {
uptimes map[ids.NodeID]time.Duration
lastUpdate map[ids.NodeID]time.Time
@@ -27,8 +33,7 @@ func newFakeUptimeState() *fakeUptimeState {
}
}
func (f *fakeUptimeState) addValidator(nodeID ids.NodeID, netID ids.ID, startTime time.Time) {
_ = netID
func (f *fakeUptimeState) addValidator(nodeID ids.NodeID, _ ids.ID, startTime time.Time) {
f.uptimes[nodeID] = 0
f.lastUpdate[nodeID] = startTime
f.startTimes[nodeID] = startTime
@@ -37,7 +42,7 @@ func (f *fakeUptimeState) addValidator(nodeID ids.NodeID, netID ids.ID, startTim
func (f *fakeUptimeState) GetUptime(nodeID ids.NodeID, _ ids.ID) (time.Duration, time.Duration, error) {
up, ok := f.uptimes[nodeID]
if !ok {
return 0, 0, errNotFound
return 0, 0, database.ErrNotFound
}
lastUpdatedDuration := time.Duration(f.lastUpdate[nodeID].Unix()) * time.Second
return up, lastUpdatedDuration, nil
@@ -45,7 +50,7 @@ func (f *fakeUptimeState) GetUptime(nodeID ids.NodeID, _ ids.ID) (time.Duration,
func (f *fakeUptimeState) SetUptime(nodeID ids.NodeID, _ ids.ID, uptime time.Duration, lastUpdated time.Time) error {
if _, ok := f.uptimes[nodeID]; !ok {
return errNotFound
return database.ErrNotFound
}
f.uptimes[nodeID] = uptime
f.lastUpdate[nodeID] = lastUpdated
@@ -55,18 +60,23 @@ func (f *fakeUptimeState) SetUptime(nodeID ids.NodeID, _ ids.ID, uptime time.Dur
func (f *fakeUptimeState) GetStartTime(nodeID ids.NodeID, _ ids.ID) (time.Time, error) {
st, ok := f.startTimes[nodeID]
if !ok {
return time.Time{}, errNotFound
return time.Time{}, database.ErrNotFound
}
return st, nil
}
var errNotFound = errTestNotFound{}
type errTestNotFound struct{}
func (errTestNotFound) Error() string { return "not found" }
func TestUptimeTrackerConnectDisconnect(t *testing.T) {
// TestUptimeTrackerLongRunningValidatorAccruesUptime is the regression test for
// the ~165M LUX reward gate. A validator that has been staked for 30 days must
// report ~100% uptime, not 0%.
//
// This test uses ONLY the shared Calculator surface (newUptimeTracker +
// CalculateUptimePercentFrom) that both the old and the fixed tracker expose, so
// it can be run against either implementation:
// - OLD tracker: returns ~0.0 — stored upDuration is 0 and the tracker had no
// baseline for the un-measured window, so upDuration/total = 0/30d = 0.
// - FIXED tracker: returns ~1.0 — before tracking begins, a validator is
// assumed online since its last persisted update (avalanchego semantics).
func TestUptimeTrackerLongRunningValidatorAccruesUptime(t *testing.T) {
require := require.New(t)
state := newFakeUptimeState()
@@ -76,87 +86,240 @@ func TestUptimeTrackerConnectDisconnect(t *testing.T) {
now := time.Now()
clk := func() time.Time { return now }
state.addValidator(nodeID, netID, now.Add(-time.Hour))
startTime := now.Add(-30 * 24 * time.Hour) // staked 30 days ago
state.addValidator(nodeID, netID, startTime)
tracker := newUptimeTracker(state, netID, clk)
// Before connect: 0% uptime.
pct, err := tracker.CalculateUptimePercentFrom(nodeID, netID, startTime)
require.NoError(err)
require.InDelta(1.0, pct, 0.001, "a continuously-staked validator must report ~100%% uptime, not 0%%")
}
// TestUptimeTrackerStartTrackingBaselines verifies that StartTracking credits the
// un-measured pre-tracking window (assuming the validator was online) and moves
// the tracker into live-tracking mode.
func TestUptimeTrackerStartTrackingBaselines(t *testing.T) {
require := require.New(t)
state := newFakeUptimeState()
netID := ids.GenerateTestID()
nodeID := ids.GenerateTestNodeID()
now := time.Now().Truncate(time.Second)
clk := func() time.Time { return now }
startTime := now.Add(-time.Hour)
state.addValidator(nodeID, netID, startTime)
tracker := newUptimeTracker(state, netID, clk)
require.False(tracker.StartedTracking())
require.NoError(tracker.StartTracking([]ids.NodeID{nodeID}))
require.True(tracker.StartedTracking())
// The hour since lastUpdated (== startTime) is baked into the persisted
// up-duration, and lastUpdated advanced to now.
require.Equal(time.Hour, state.uptimes[nodeID])
require.Equal(now.Unix(), state.lastUpdate[nodeID].Unix())
}
// TestUptimeTrackerContinuouslyConnectedClimbs is the core behavioral fix: a
// validator that connects (during bootstrap) and stays connected accrues uptime
// over time WITHOUT ever disconnecting, and IsConnected reports true.
func TestUptimeTrackerContinuouslyConnectedClimbs(t *testing.T) {
require := require.New(t)
state := newFakeUptimeState()
netID := ids.GenerateTestID()
nodeID := ids.GenerateTestNodeID()
now := time.Now().Truncate(time.Second)
clk := func() time.Time { return now }
startTime := now
state.addValidator(nodeID, netID, startTime)
tracker := newUptimeTracker(state, netID, clk)
// Peer connects during bootstrap, before tracking starts.
tracker.Connect(nodeID)
require.True(tracker.IsConnected(nodeID))
// Normal operations begin.
require.NoError(tracker.StartTracking([]ids.NodeID{nodeID}))
// One hour passes with the validator continuously connected — no Disconnect.
now = now.Add(time.Hour)
pct, err := tracker.CalculateUptimePercent(nodeID, netID)
require.NoError(err)
require.InDelta(0.0, pct, 0.01)
require.InDelta(1.0, pct, 0.001, "continuously-connected validator must climb to ~100%%")
require.True(tracker.IsConnected(nodeID))
// Connect.
tracker.Connect(nodeID)
// Advance clock by 30 minutes.
now = now.Add(30 * time.Minute)
// While connected: should show ~50% (30min connected / 60min+30min total).
// Two hours in, still ~100%.
now = now.Add(time.Hour)
pct, err = tracker.CalculateUptimePercent(nodeID, netID)
require.NoError(err)
// 30min / 90min = 0.333...
require.InDelta(0.333, pct, 0.01)
// Disconnect.
require.NoError(tracker.Disconnect(nodeID))
// State should now have 30 minutes of uptime persisted.
require.Equal(30*time.Minute, state.uptimes[nodeID])
// After disconnect, no live connection bonus.
pct, err = tracker.CalculateUptimePercent(nodeID, netID)
require.NoError(err)
require.InDelta(0.333, pct, 0.01)
require.InDelta(1.0, pct, 0.001)
}
func TestUptimeTrackerDoubleConnect(t *testing.T) {
// TestUptimeTrackerConnectDisconnectFlush verifies that, once tracking, a
// connected session is flushed into persistent state on Disconnect.
func TestUptimeTrackerConnectDisconnectFlush(t *testing.T) {
require := require.New(t)
state := newFakeUptimeState()
netID := ids.GenerateTestID()
nodeID := ids.GenerateTestNodeID()
now := time.Now()
now := time.Now().Truncate(time.Second)
clk := func() time.Time { return now }
state.addValidator(nodeID, netID, now.Add(-time.Hour))
startTime := now
state.addValidator(nodeID, netID, startTime)
tracker := newUptimeTracker(state, netID, clk)
require.NoError(tracker.StartTracking([]ids.NodeID{nodeID}))
require.Equal(time.Duration(0), state.uptimes[nodeID])
tracker.Connect(nodeID)
tracker.Connect(nodeID) // second connect should be no-op
now = now.Add(30 * time.Minute)
mutated, err := tracker.Disconnect(nodeID)
require.NoError(err)
require.True(mutated) // a tracked validator's session flush writes uptime
now = now.Add(10 * time.Minute)
require.NoError(tracker.Disconnect(nodeID))
require.Equal(30*time.Minute, state.uptimes[nodeID])
require.False(tracker.IsConnected(nodeID))
// Should be exactly 10 minutes, not 20.
require.Equal(10*time.Minute, state.uptimes[nodeID])
// After disconnect, no further live-session bonus; percent reflects 30m/60m.
now = now.Add(30 * time.Minute)
pct, err := tracker.CalculateUptimePercent(nodeID, netID)
require.NoError(err)
require.InDelta(0.5, pct, 0.001)
}
func TestUptimeTrackerShutdown(t *testing.T) {
// TestUptimeTrackerDisconnectBeforeTrackingIsNoWrite is the regression test for
// the empty-commit elimination (RED round-2 LOW). A peer disconnect during
// bootstrap — before StartTracking — must report mutated=false so VM.Disconnected
// skips an empty state.Commit (a full-write+fsync). Under bootstrap churn that
// commit ran on every disconnect for no reason.
func TestUptimeTrackerDisconnectBeforeTrackingIsNoWrite(t *testing.T) {
require := require.New(t)
state := newFakeUptimeState()
netID := ids.GenerateTestID()
nodeA := ids.GenerateTestNodeID()
nodeB := ids.GenerateTestNodeID()
nodeID := ids.GenerateTestNodeID()
now := time.Now()
now := time.Now().Truncate(time.Second)
clk := func() time.Time { return now }
state.addValidator(nodeA, netID, now.Add(-time.Hour))
state.addValidator(nodeB, netID, now.Add(-time.Hour))
state.addValidator(nodeID, netID, now)
tracker := newUptimeTracker(state, netID, clk)
// Deliberately NO StartTracking — we are still bootstrapping.
tracker.Connect(nodeA)
tracker.Connect(nodeB)
tracker.Connect(nodeID)
now = now.Add(30 * time.Minute)
now = now.Add(5 * time.Minute)
require.NoError(tracker.Shutdown())
require.Equal(5*time.Minute, state.uptimes[nodeA])
require.Equal(5*time.Minute, state.uptimes[nodeB])
mutated, err := tracker.Disconnect(nodeID)
require.NoError(err)
require.False(mutated) // no write → VM.Disconnected must skip Commit
require.False(tracker.IsConnected(nodeID))
require.Equal(time.Duration(0), state.uptimes[nodeID]) // untouched
}
// TestUptimeTrackerDisconnectedValidatorGetsZero verifies that, once tracking, a
// validator that never connects earns 0% uptime (it is offline from this node's
// perspective) — the property that lets the reward gate withhold rewards.
func TestUptimeTrackerDisconnectedValidatorGetsZero(t *testing.T) {
require := require.New(t)
state := newFakeUptimeState()
netID := ids.GenerateTestID()
nodeID := ids.GenerateTestNodeID()
now := time.Now().Truncate(time.Second)
clk := func() time.Time { return now }
startTime := now
state.addValidator(nodeID, netID, startTime)
tracker := newUptimeTracker(state, netID, clk)
require.NoError(tracker.StartTracking([]ids.NodeID{nodeID}))
now = now.Add(time.Hour) // an hour passes, never connected
pct, err := tracker.CalculateUptimePercent(nodeID, netID)
require.NoError(err)
require.InDelta(0.0, pct, 0.001, "never-connected validator (while tracking) must earn 0%%")
require.False(tracker.IsConnected(nodeID))
}
// TestUptimeTrackerStopTrackingFlushesAll verifies that StopTracking persists all
// connected validators' sessions and leaves tracking mode.
func TestUptimeTrackerStopTrackingFlushesAll(t *testing.T) {
require := require.New(t)
state := newFakeUptimeState()
netID := ids.GenerateTestID()
now := time.Now().Truncate(time.Second)
clk := func() time.Time { return now }
const numValidators = 10
nodeIDs := make([]ids.NodeID, numValidators)
for i := range nodeIDs {
nodeIDs[i] = ids.GenerateTestNodeID()
state.addValidator(nodeIDs[i], netID, now)
}
tracker := newUptimeTracker(state, netID, clk)
require.NoError(tracker.StartTracking(nodeIDs))
for _, nid := range nodeIDs {
tracker.Connect(nid)
}
now = now.Add(3 * time.Minute)
require.NoError(tracker.StopTracking(nodeIDs))
require.False(tracker.StartedTracking())
for _, nid := range nodeIDs {
require.Equal(3*time.Minute, state.uptimes[nid])
}
}
// TestUptimeTrackerDoubleStartTracking verifies StartTracking is not re-entrant.
func TestUptimeTrackerDoubleStartTracking(t *testing.T) {
require := require.New(t)
state := newFakeUptimeState()
netID := ids.GenerateTestID()
nodeID := ids.GenerateTestNodeID()
now := time.Now().Truncate(time.Second)
state.addValidator(nodeID, netID, now)
tracker := newUptimeTracker(state, netID, func() time.Time { return now })
require.NoError(tracker.StartTracking([]ids.NodeID{nodeID}))
require.ErrorIs(tracker.StartTracking([]ids.NodeID{nodeID}), errAlreadyStartedTracking)
}
// TestUptimeTrackerStopWithoutStart verifies StopTracking errors before start.
func TestUptimeTrackerStopWithoutStart(t *testing.T) {
require := require.New(t)
state := newFakeUptimeState()
netID := ids.GenerateTestID()
now := time.Now().Truncate(time.Second)
tracker := newUptimeTracker(state, netID, func() time.Time { return now })
require.ErrorIs(tracker.StopTracking(nil), errNotStartedTracking)
}
// TestUptimeTrackerUnknownValidator verifies non-validators are handled safely:
// StartTracking/Connect/Disconnect skip them without error, and a percent query
// surfaces the not-found error.
func TestUptimeTrackerUnknownValidator(t *testing.T) {
require := require.New(t)
@@ -164,21 +327,28 @@ func TestUptimeTrackerUnknownValidator(t *testing.T) {
netID := ids.GenerateTestID()
unknownNode := ids.GenerateTestNodeID()
now := time.Now()
now := time.Now().Truncate(time.Second)
clk := func() time.Time { return now }
tracker := newUptimeTracker(state, netID, clk)
// Connect an unknown validator. Disconnect should not error.
// StartTracking must not fail on a node that has no state record.
require.NoError(tracker.StartTracking([]ids.NodeID{unknownNode}))
// Connecting then disconnecting an unknown node is a no-op, not an error, and
// must NOT report a state mutation (no uptime record to write).
tracker.Connect(unknownNode)
now = now.Add(time.Minute)
require.NoError(tracker.Disconnect(unknownNode))
mutated, err := tracker.Disconnect(unknownNode)
require.NoError(err)
require.False(mutated)
// CalculateUptimePercent for unknown validator should error.
_, err := tracker.CalculateUptimePercent(unknownNode, netID)
// A percent query for an unknown validator surfaces the error.
_, err = tracker.CalculateUptimePercent(unknownNode, netID)
require.Error(err)
}
// TestUptimeTrackerWrongNet verifies a query for a different network returns 0.
func TestUptimeTrackerWrongNet(t *testing.T) {
require := require.New(t)
@@ -187,41 +357,52 @@ func TestUptimeTrackerWrongNet(t *testing.T) {
otherNet := ids.GenerateTestID()
nodeID := ids.GenerateTestNodeID()
now := time.Now()
now := time.Now().Truncate(time.Second)
clk := func() time.Time { return now }
state.addValidator(nodeID, netID, now.Add(-time.Hour))
tracker := newUptimeTracker(state, netID, clk)
// Querying the wrong net should return 0.
pct, err := tracker.CalculateUptimePercent(nodeID, otherNet)
require.NoError(err)
require.Equal(0.0, pct)
up, total, err := tracker.CalculateUptime(nodeID, otherNet)
require.NoError(err)
require.Equal(time.Duration(0), up)
require.Equal(time.Duration(0), total)
}
func TestUptimeTrackerDisconnectWithoutConnect(t *testing.T) {
// TestUptimeTrackerDoubleConnect verifies a duplicate Connect keeps the original
// connection time (repeated router dispatch cannot inflate uptime).
func TestUptimeTrackerDoubleConnect(t *testing.T) {
require := require.New(t)
state := newFakeUptimeState()
netID := ids.GenerateTestID()
nodeID := ids.GenerateTestNodeID()
now := time.Now()
now := time.Now().Truncate(time.Second)
clk := func() time.Time { return now }
state.addValidator(nodeID, netID, now.Add(-time.Hour))
state.addValidator(nodeID, netID, now)
tracker := newUptimeTracker(state, netID, clk)
require.NoError(tracker.StartTracking([]ids.NodeID{nodeID}))
// Disconnect without prior connect should be no-op.
require.NoError(tracker.Disconnect(nodeID))
require.Equal(time.Duration(0), state.uptimes[nodeID])
tracker.Connect(nodeID)
now = now.Add(5 * time.Minute)
tracker.Connect(nodeID) // duplicate — must NOT reset the 5-minute-old session
now = now.Add(5 * time.Minute)
mutated, err := tracker.Disconnect(nodeID)
require.NoError(err)
require.True(mutated)
// Ten minutes total, not five.
require.Equal(10*time.Minute, state.uptimes[nodeID])
}
// --- Additional edge-case and inversion tests ---
// TestUptimeTrackerRapidConnectDisconnect verifies that rapid Connect/Disconnect
// cycling does not accumulate phantom uptime. Each cycle should only account
// for the exact clock delta during that connection.
// TestUptimeTrackerRapidConnectDisconnect verifies rapid cycling never fabricates
// uptime beyond the exact connected intervals.
func TestUptimeTrackerRapidConnectDisconnect(t *testing.T) {
require := require.New(t)
@@ -229,179 +410,51 @@ func TestUptimeTrackerRapidConnectDisconnect(t *testing.T) {
netID := ids.GenerateTestID()
nodeID := ids.GenerateTestNodeID()
now := time.Now()
now := time.Now().Truncate(time.Second)
clk := func() time.Time { return now }
state.addValidator(nodeID, netID, now.Add(-time.Hour))
state.addValidator(nodeID, netID, now)
tracker := newUptimeTracker(state, netID, clk)
require.NoError(tracker.StartTracking([]ids.NodeID{nodeID}))
// Rapid connect/disconnect 100 times with no clock advance.
// 100 cycles with no clock advance — zero accrual.
for i := 0; i < 100; i++ {
tracker.Connect(nodeID)
require.NoError(tracker.Disconnect(nodeID))
_, err := tracker.Disconnect(nodeID)
require.NoError(err)
}
// No time passed, so uptime should be 0.
require.Equal(time.Duration(0), state.uptimes[nodeID])
// Now do 50 cycles with 1ms advance each.
// 50 cycles, each holding the connection for one second.
for i := 0; i < 50; i++ {
tracker.Connect(nodeID)
now = now.Add(1 * time.Millisecond)
require.NoError(tracker.Disconnect(nodeID))
now = now.Add(time.Second)
_, err := tracker.Disconnect(nodeID)
require.NoError(err)
}
// Should be exactly 50ms of uptime.
require.Equal(50*time.Millisecond, state.uptimes[nodeID])
require.Equal(50*time.Second, state.uptimes[nodeID])
}
// TestUptimeTrackerShutdownFlushesAll verifies that Shutdown flushes all
// currently connected validators and leaves the connected map empty.
func TestUptimeTrackerShutdownFlushesAll(t *testing.T) {
require := require.New(t)
state := newFakeUptimeState()
netID := ids.GenerateTestID()
now := time.Now()
clk := func() time.Time { return now }
const numValidators = 10
nodeIDs := make([]ids.NodeID, numValidators)
for i := range nodeIDs {
nodeIDs[i] = ids.GenerateTestNodeID()
state.addValidator(nodeIDs[i], netID, now.Add(-time.Hour))
}
tracker := newUptimeTracker(state, netID, clk)
// Connect all
for _, nid := range nodeIDs {
tracker.Connect(nid)
}
now = now.Add(3 * time.Minute)
require.NoError(tracker.Shutdown())
// All should have 3 minutes of uptime
for _, nid := range nodeIDs {
require.Equal(3*time.Minute, state.uptimes[nid])
}
// Connected map should be empty after shutdown
tracker.mu.RLock()
require.Len(tracker.connected, 0)
tracker.mu.RUnlock()
}
// TestUptimeTrackerNeverConnected verifies that a validator that was never
// connected has 0% uptime.
func TestUptimeTrackerNeverConnected(t *testing.T) {
// TestUptimeTrackerConcurrent races Connect/Disconnect/reads and StartTracking to
// assert there are no data races (run with -race).
func TestUptimeTrackerConcurrent(t *testing.T) {
require := require.New(t)
state := newFakeUptimeState()
netID := ids.GenerateTestID()
nodeID := ids.GenerateTestNodeID()
now := time.Now()
clk := func() time.Time { return now }
// Validator has been registered for 1 hour but never connected
state.addValidator(nodeID, netID, now.Add(-time.Hour))
tracker := newUptimeTracker(state, netID, clk)
pct, err := tracker.CalculateUptimePercent(nodeID, netID)
require.NoError(err)
require.Equal(0.0, pct, "never-connected validator must have 0%% uptime")
}
// TestUptimeTrackerAlwaysConnected verifies that a validator connected for
// the entire tracking period reports 100% uptime.
func TestUptimeTrackerAlwaysConnected(t *testing.T) {
require := require.New(t)
state := newFakeUptimeState()
netID := ids.GenerateTestID()
nodeID := ids.GenerateTestNodeID()
now := time.Now()
clk := func() time.Time { return now }
startTime := now
state.addValidator(nodeID, netID, startTime)
tracker := newUptimeTracker(state, netID, clk)
// Connect immediately at start
tracker.Connect(nodeID)
// Advance clock by 1 hour
now = now.Add(time.Hour)
pct, err := tracker.CalculateUptimePercent(nodeID, netID)
require.NoError(err)
require.InDelta(1.0, pct, 0.001, "always-connected validator must have ~100%% uptime")
}
// TestUptimeTrackerConcurrentConnect verifies that concurrent Connect calls
// from multiple goroutines do not cause data races or phantom uptime.
func TestUptimeTrackerConcurrentConnect(t *testing.T) {
require := require.New(t)
state := newFakeUptimeState()
netID := ids.GenerateTestID()
nodeID := ids.GenerateTestNodeID()
now := time.Now()
clk := func() time.Time { return now }
state.addValidator(nodeID, netID, now.Add(-time.Hour))
tracker := newUptimeTracker(state, netID, clk)
// Race 50 goroutines calling Connect on the same node.
const goroutines = 50
var wg sync.WaitGroup
wg.Add(goroutines)
for i := 0; i < goroutines; i++ {
go func() {
defer wg.Done()
tracker.Connect(nodeID)
}()
}
wg.Wait()
// Should have exactly one entry in connected map.
tracker.mu.RLock()
_, exists := tracker.connected[nodeID]
require.True(exists)
tracker.mu.RUnlock()
// Advance clock and disconnect
now = now.Add(5 * time.Minute)
require.NoError(tracker.Disconnect(nodeID))
// Uptime should be exactly 5 minutes, not 5*50 minutes.
require.Equal(5*time.Minute, state.uptimes[nodeID])
}
// TestUptimeTrackerConcurrentConnectDisconnect races Connect and Disconnect
// from multiple goroutines on the same node.
func TestUptimeTrackerConcurrentConnectDisconnect(t *testing.T) {
require := require.New(t)
state := newFakeUptimeState()
netID := ids.GenerateTestID()
nodeID := ids.GenerateTestNodeID()
now := time.Now()
mu := sync.Mutex{}
var clkMu sync.Mutex
now := time.Now().Truncate(time.Second)
clk := func() time.Time {
mu.Lock()
defer mu.Unlock()
clkMu.Lock()
defer clkMu.Unlock()
return now
}
state.addValidator(nodeID, netID, now.Add(-time.Hour))
state.addValidator(nodeID, netID, now)
tracker := newUptimeTracker(state, netID, clk)
require.NoError(tracker.StartTracking([]ids.NodeID{nodeID}))
const goroutines = 100
var wg sync.WaitGroup
@@ -409,46 +462,22 @@ func TestUptimeTrackerConcurrentConnectDisconnect(t *testing.T) {
for i := 0; i < goroutines; i++ {
go func(idx int) {
defer wg.Done()
if idx%2 == 0 {
switch idx % 4 {
case 0:
tracker.Connect(nodeID)
} else {
_ = tracker.Disconnect(nodeID)
case 1:
_, _ = tracker.Disconnect(nodeID)
case 2:
_, _ = tracker.CalculateUptimePercent(nodeID, netID)
case 3:
_ = tracker.IsConnected(nodeID)
}
}(i)
}
wg.Wait()
// Should not panic. Final state depends on ordering but must be consistent.
// Clean up: ensure we can still shutdown.
mu.Lock()
clkMu.Lock()
now = now.Add(time.Minute)
mu.Unlock()
require.NoError(tracker.Shutdown())
}
func TestUptimeTrackerCalculateUptimePercentFrom(t *testing.T) {
require := require.New(t)
state := newFakeUptimeState()
netID := ids.GenerateTestID()
nodeID := ids.GenerateTestNodeID()
now := time.Now()
clk := func() time.Time { return now }
startTime := now.Add(-2 * time.Hour)
state.addValidator(nodeID, netID, startTime)
tracker := newUptimeTracker(state, netID, clk)
// Connect for 1 hour.
tracker.Connect(nodeID)
now = now.Add(time.Hour)
require.NoError(tracker.Disconnect(nodeID))
// Total time is 3 hours (2h before + 1h connected).
// Connected for 1 hour. Rate = 1/3.
// From startTime: same rate applies.
pct, err := tracker.CalculateUptimePercentFrom(nodeID, netID, startTime)
require.NoError(err)
require.InDelta(0.333, pct, 0.01)
clkMu.Unlock()
require.NoError(tracker.StopTracking([]ids.NodeID{nodeID}))
}
+118 -22
View File
@@ -100,6 +100,23 @@ type VM struct {
chainID ids.ID
state state.State
// stateLock serializes every commit of the shared platform state (state.write)
// that originates OUTSIDE the consensus engine's serialized accept path:
// - a block DECISION (block/executor Block.Accept/Reject → state.CommitBatch);
// the executor manager holds THIS lock (passed as &vm.stateLock),
// - a peer Disconnect (tracker.Disconnect → state.SetUptime, then state.Commit),
// - normal-ops Start/StopTracking uptime flushes (state.SetUptime + state.Commit).
// The engine invokes VM.Accept as a lock-free call-out (its t.mu is released
// before the call-out per its lock discipline), and peer connect/disconnect run
// on the node's peer-lifecycle goroutine, so nothing else serializes these
// writers against one another. Without this lock a disconnect's state.Commit
// races an accept's state.CommitBatch inside state.write() → Go "concurrent map
// writes" fatal. This is the avalanchego ctx.Lock invariant (accept serialized
// with engine.Connected/Disconnected), scoped to the state the platform VM owns.
// It is DISTINCT from vm.lock (which guards API/service reads): a separate lock
// keeps the accept path off the API lock and avoids any ordering coupling.
stateLock sync.Mutex
fx fx.Fx
// Bootstrapped remembers if this chain has finished bootstrapping or not
@@ -260,9 +277,17 @@ func (vm *VM) Initialize(
validatorManager := pvalidators.NewManager(vm.Internal, vm.state, vm.metrics, &vm.nodeClock)
vm.State = validatorManager
utxoHandler := utxo.NewHandler(context.Background(), &vm.nodeClock, vm.fx)
// Create uptime manager - use the configured UptimeLockedCalculator which
// delegates to its fallback calculator (NoOp by default, but tests can
// configure ZeroUptimeCalculator for "never connected" scenarios)
// Create the real uptime tracker for the primary network NOW, at Initialize,
// so peer Connect events delivered during bootstrap are captured — the
// connected map must already be populated by the time StartTracking runs at
// normal-operations start. Register it with the thread-safe LockedCalculator,
// through which both the API (service.go getCurrentValidators) and the reward
// gate (block/executor/options.go prefersCommit) read uptime.
vm.tracker = newUptimeTracker(vm.state, constants.PrimaryNetworkID, vm.nodeClock.Time)
if err := vm.UptimeLockedCalculator.SetCalculator(constants.PrimaryNetworkID, vm.tracker); err != nil {
return fmt.Errorf("failed to register uptime tracker: %w", err)
}
vm.uptimeManager = vm.UptimeLockedCalculator
txExecutorBackend := &txexecutor.Backend{
@@ -294,6 +319,7 @@ func (vm *VM) Initialize(
vm.state,
txExecutorBackend,
validatorManager,
&vm.stateLock,
)
txVerifier := network.NewLockedTxVerifier(&vm.lock, vm.manager)
@@ -531,6 +557,25 @@ func (vm *VM) createNet(netID ids.ID) error {
func (vm *VM) onBootstrapStarted() error {
vm.bootstrapped.Set(false)
vm.bootstrappedConsensus.Set(false)
// On a normal-ops → re-bootstrap transition, flush and stop uptime tracking
// so connected sessions are persisted before we stop measuring. This is a
// no-op on the first bootstrap (tracking hasn't started yet). StopTracking
// flushes uptime into shared state (state.SetUptime → state.write), so hold
// stateLock to serialize it with any block accept still in flight.
if err := func() error {
vm.stateLock.Lock()
defer vm.stateLock.Unlock()
if vm.tracker != nil && vm.tracker.StartedTracking() {
primaryVdrIDs := vm.Validators.GetValidatorIDs(constants.PrimaryNetworkID)
if err := vm.tracker.StopTracking(primaryVdrIDs); err != nil {
return err
}
}
return nil
}(); err != nil {
return err
}
return vm.fx.Bootstrapping()
}
@@ -546,16 +591,32 @@ func (vm *VM) onReady() error {
return err
}
// Create and register the real uptime tracker for the primary network.
vm.tracker = newUptimeTracker(vm.state, constants.PrimaryNetworkID, vm.nodeClock.Time)
if err := vm.UptimeLockedCalculator.SetCalculator(constants.PrimaryNetworkID, vm.tracker); err != nil {
return err
}
vm.log.Info("uptime tracker registered for primary network")
// Begin tracking validator uptime for the primary network. The tracker was
// created and registered at Initialize (so bootstrap-time peer connections
// were already captured); StartTracking baselines every current validator's
// uptime record and switches the tracker into live-tracking mode. Mirrors
// avalanchego's onNormalOperationsStarted.
//
// StartTracking (state.SetUptime) and the trailing state.Commit both write
// shared state, so hold stateLock across them to serialize with any block
// accept (Block.Accept holds the same lock) — the same guard as the peer
// Disconnect path.
if err := func() error {
vm.stateLock.Lock()
defer vm.stateLock.Unlock()
if vm.tracker != nil && !vm.tracker.StartedTracking() {
primaryVdrIDs := vm.Validators.GetValidatorIDs(constants.PrimaryNetworkID)
if err := vm.tracker.StartTracking(primaryVdrIDs); err != nil {
return err
}
vm.log.Info("uptime tracking started for primary network",
log.Int("validators", len(primaryVdrIDs)))
}
// Commit state BEFORE starting background goroutines to avoid race conditions
// between state readers (forwardNotifications) and state writers (Commit)
if err := vm.state.Commit(); err != nil {
// Commit state BEFORE starting background goroutines to avoid race conditions
// between state readers (forwardNotifications) and state writers (Commit)
return vm.state.Commit()
}(); err != nil {
return err
}
@@ -592,13 +653,25 @@ func (vm *VM) Shutdown(context.Context) error {
vm.onShutdownCtxCancel()
if vm.tracker != nil {
if err := vm.tracker.Shutdown(); err != nil {
return err
}
if err := vm.state.Commit(); err != nil {
return err
// Flush uptime for all primary-network validators before closing state, so
// connected sessions are durably persisted across the restart. StopTracking
// (state.SetUptime) + state.Commit write shared state, so hold stateLock to
// serialize with any block accept still draining as the chain stops.
if err := func() error {
vm.stateLock.Lock()
defer vm.stateLock.Unlock()
if vm.tracker != nil && vm.tracker.StartedTracking() {
primaryVdrIDs := vm.Validators.GetValidatorIDs(constants.PrimaryNetworkID)
if err := vm.tracker.StopTracking(primaryVdrIDs); err != nil {
return err
}
if err := vm.state.Commit(); err != nil {
return err
}
}
return nil
}(); err != nil {
return err
}
var errs []error
@@ -762,14 +835,37 @@ func (vm *VM) Connected(ctx context.Context, nodeID ids.NodeID, nodeVersion *cha
}
func (vm *VM) Disconnected(ctx context.Context, nodeID ids.NodeID) error {
// This runs on the node's peer-lifecycle goroutine, which the consensus
// engine does NOT serialize against block accept. tracker.Disconnect flushes
// the peer's uptime into shared state (state.SetUptime) and state.Commit
// persists the whole diff (state.write). Hold stateLock so both are atomic
// w.r.t. a concurrent block accept (Block.Accept holds the same lock);
// otherwise state.write races the acceptor's state.write → concurrent map
// writes fatal. The p2p Network.Disconnected below touches only the p2p peer
// set (its own lock), so it stays OUTSIDE stateLock to keep the critical
// section to the state commit.
//
// Commit ONLY when the flush actually wrote uptime state. Before StartTracking
// (bootstrap churn) and for non-validator peers, Disconnect is a no-op, and an
// unconditional Commit would be an empty full-write+fsync on every such
// disconnect. A no-write disconnect leaves the state clean, so skipping Commit
// is correct — every writer under stateLock (block accept, Start/StopTracking)
// commits its own diff, so there is never an orphaned write relying on us.
vm.stateLock.Lock()
if vm.tracker != nil {
if err := vm.tracker.Disconnect(nodeID); err != nil {
mutated, err := vm.tracker.Disconnect(nodeID)
if err != nil {
vm.stateLock.Unlock()
return err
}
if mutated {
if err := vm.state.Commit(); err != nil {
vm.stateLock.Unlock()
return err
}
}
}
if err := vm.state.Commit(); err != nil {
return err
}
vm.stateLock.Unlock()
return vm.Network.Disconnected(ctx, nodeID)
}
+60 -25
View File
@@ -25,20 +25,21 @@ const (
)
var (
errUnsignedChild = errors.New("expected child to be signed")
errUnexpectedBlockType = errors.New("unexpected proposer block type")
errInnerParentMismatch = errors.New("inner parentID didn't match expected parent")
errTimeNotMonotonic = errors.New("time must monotonically increase")
errPChainHeightNotMonotonic = errors.New("non monotonically increasing P-chain height")
errPChainHeightNotReached = errors.New("block P-chain height larger than current P-chain height")
errTimeTooAdvanced = errors.New("time is too far advanced")
errEpochMismatch = errors.New("epoch mismatch")
errProposerWindowNotStarted = errors.New("proposer window hasn't started")
errUnexpectedProposer = errors.New("unexpected proposer for current window")
errProposerMismatch = errors.New("proposer mismatch")
errProposersNotActivated = errors.New("proposers haven't been activated yet")
errPChainHeightTooLow = errors.New("block P-chain height is too low")
errNotOracle = errors.New("block is not an oracle block")
errUnsignedChild = errors.New("expected child to be signed")
errUnexpectedBlockType = errors.New("unexpected proposer block type")
errInnerParentMismatch = errors.New("inner parentID didn't match expected parent")
errClassicalProposerUnderStrictPQ = errors.New("classical (secp256k1) proposer identity refused on a strict-PQ chain")
errTimeNotMonotonic = errors.New("time must monotonically increase")
errPChainHeightNotMonotonic = errors.New("non monotonically increasing P-chain height")
errPChainHeightNotReached = errors.New("block P-chain height larger than current P-chain height")
errTimeTooAdvanced = errors.New("time is too far advanced")
errEpochMismatch = errors.New("epoch mismatch")
errProposerWindowNotStarted = errors.New("proposer window hasn't started")
errUnexpectedProposer = errors.New("unexpected proposer for current window")
errProposerMismatch = errors.New("proposer mismatch")
errProposersNotActivated = errors.New("proposers haven't been activated yet")
errPChainHeightTooLow = errors.New("block P-chain height is too low")
errNotOracle = errors.New("block is not an oracle block")
)
// OracleBlock is a block that can return multiple child options
@@ -124,6 +125,18 @@ func (p *postForkCommonComponents) Verify(
parentEpoch chain.Epoch,
child *postForkBlock,
) error {
// STRICT-PQ PROFILE GATE (fail-closed, UNCONDITIONAL — before the Ready gate,
// so it also holds during bootstrap/state-sync when the proposer-window check
// below is skipped). A chain whose own proposer signs with ML-DSA-65
// (StakingMLDSASigner set) is strict-PQ: its validator set is ML-DSA-keyed, so
// a block carrying a CLASSICAL secp256k1 proposer identity can never be a
// legitimate proposer and must be refused outright — never relying on the
// 20-byte NodeID collision bound or upstream validator-set enforcement to catch
// it. Mirrors contract.RefuseUnderStrictPQ: one gate, one place.
if p.vm.StakingMLDSASigner != nil && child.SignedBlock.HasClassicalProposer() {
return errClassicalProposerUnderStrictPQ
}
if err := verifyIsNotOracleBlock(ctx, p.innerBlk); err != nil {
return err
}
@@ -233,10 +246,11 @@ func (p *postForkCommonComponents) Verify(
// sub-window timestamp — zero behaviour change on a live chain;
// - a snapped time is strictly > parent (monotonic, no errTimeNotMonotonic) and ≤ now (never
// errTimeTooAdvanced).
//
// It is a pure function of (parentTimestamp, now) so it is idempotent for any two calls in the
// same slot — the property the liveness fix relies on.
func slotSnappedChildTimestamp(parentTimestamp, now time.Time) time.Time {
ts := now.Truncate(time.Second)
ts := now.Truncate(proposer.TimestampGranularity())
if ts.Before(parentTimestamp) {
return parentTimestamp
}
@@ -287,6 +301,12 @@ func (p *postForkCommonComponents) buildChild(
_ = pChainHeight
contextPChainHeight := epoch.PChainHeight
// The inner VM builds on ITS OWN head, and Verify above requires the child's inner
// parent to be exactly p.innerBlk. Anchor the two together before delegating.
if err := p.vm.anchorInnerBuildParent(ctx, parentID, p.innerBlk.ID()); err != nil {
return nil, err
}
var innerBlock chain.Block
if p.vm.blockBuilderVM != nil {
builtBlock, err := p.vm.blockBuilderVM.BuildBlockWithRuntime(ctx, &runtime.Runtime{
@@ -307,16 +327,31 @@ func (p *postForkCommonComponents) buildChild(
// Build the child
var statelessChild block.SignedBlock
if shouldBuildSignedBlock {
statelessChild, err = block.Build(
parentID,
newTimestamp,
pChainHeight,
epoch,
p.vm.StakingCertLeaf,
innerBlock.Bytes(),
p.vm.rt.ChainID,
p.vm.StakingLeafSigner,
)
if p.vm.StakingMLDSASigner != nil {
// Strict-PQ: sign with ML-DSA-65 and stamp the raw ML-DSA pubkey so the
// block's Proposer() == the windower's ML-DSA-keyed election.
statelessChild, err = block.BuildMLDSA(
parentID,
newTimestamp,
pChainHeight,
epoch,
p.vm.StakingMLDSAPub,
innerBlock.Bytes(),
p.vm.rt.ChainID,
p.vm.StakingMLDSASigner,
)
} else {
statelessChild, err = block.Build(
parentID,
newTimestamp,
pChainHeight,
epoch,
p.vm.StakingCertLeaf,
innerBlock.Bytes(),
p.vm.rt.ChainID,
p.vm.StakingLeafSigner,
)
}
} else {
statelessChild, err = block.BuildUnsigned(
parentID,
+82 -17
View File
@@ -9,6 +9,7 @@ import (
"time"
"github.com/luxfi/crypto/hash"
"github.com/luxfi/crypto/mldsa"
"github.com/luxfi/ids"
"github.com/luxfi/node/staking"
"github.com/luxfi/zap"
@@ -17,8 +18,26 @@ import (
var (
_ SignedBlock = (*statelessBlock)(nil)
errUnexpectedSignature = errors.New("signature provided when none was expected")
errInvalidCertificate = errors.New("invalid certificate")
errUnexpectedSignature = errors.New("signature provided when none was expected")
errInvalidCertificate = errors.New("invalid certificate")
errUnknownProposerScheme = errors.New("proposervm block: unknown proposer identity scheme")
errMLDSAProposerSigInvalid = errors.New("proposervm block: ML-DSA proposer signature invalid")
)
// Proposer-identity scheme tags stored as the FIRST byte of the offCert slot,
// so a block is self-describing about which primitive proves its proposer.
// They are the SAME bytes as the canonical wire NodeID scheme (ids.NodeIDScheme):
// 0x90 classical secp256k1/ECDSA TLS leaf, 0x42 strict-PQ ML-DSA-65. Keeping the
// tag in the block means a verifier dispatches to the correct verifier + the
// correct NodeID derivation WITHOUT consulting the chain profile — the block
// carries its own identity discriminator, exactly like a TypedNodeID does on the
// handshake. The proposer NodeID a signed block reports MUST equal the NodeID the
// windower elected; the windower reads the P-chain validator set, whose NodeIDs
// are ids.NodeIDFromCert (classical) or DeriveMLDSA(ids.Empty, pub) (strict-PQ) —
// so the two branches below reproduce exactly those two derivations.
const (
schemeSecp256k1 = byte(ids.NodeIDSchemeSecp256k1) // 0x90
schemeMLDSA65 = byte(ids.NodeIDSchemeMLDSA65) // 0x42
)
// Epoch represents a P-Chain epoch for validator set coordination
@@ -49,6 +68,12 @@ type SignedBlock interface {
// signed this block, [ids.EmptyNodeID] will be returned.
Proposer() ids.NodeID
// HasClassicalProposer reports whether this block carries a CLASSICAL
// (secp256k1/ECDSA) proposer identity. A strict-PQ chain refuses such a block
// — its proposer must be ML-DSA-65 to match the ML-DSA-keyed validator set.
// Unsigned blocks (transition / single-validator) return false.
HasClassicalProposer() bool
// Data Availability fields (v1.1 spec)
DARoot() [32]byte // Root of DA commitments
WitnessRoot() [32]byte // Root of witnesses/proofs
@@ -66,7 +91,9 @@ type statelessBlock struct {
id ids.ID
timestamp time.Time
cert *staking.Certificate
scheme byte // proposer-identity scheme (0 for unsigned)
cert *staking.Certificate // set for the classical (secp256k1) scheme
mldsaPub *mldsa.PublicKey // set for the strict-PQ (ML-DSA-65) scheme
proposer ids.NodeID
bytes []byte // full signed bytes = unsigned ‖ sig
}
@@ -117,27 +144,51 @@ func (b *statelessBlock) initialize(bytes []byte) error {
}
root := b.msg.Root()
b.timestamp = time.Unix(root.Int64(offTimestamp), 0)
b.timestamp = time.UnixMilli(root.Int64(offTimestamp))
certBytes := root.Bytes(offCert)
if len(certBytes) == 0 {
// Proposer-identity slot: [scheme:1B | identity]. Empty ⇒ unsigned block.
idSlot := root.Bytes(offCert)
if len(idSlot) == 0 {
return nil
}
b.cert, err = staking.ParseCertificate(certBytes)
if err != nil {
return fmt.Errorf("%w: %w", errInvalidCertificate, err)
b.scheme = idSlot[0]
identity := idSlot[1:]
switch b.scheme {
case schemeMLDSA65:
// Strict-PQ: identity is the raw ML-DSA-65 public key. Derive the proposer
// NodeID the SAME way the node derives its own canonical NodeID and the
// windower reads it — DeriveMLDSA over the empty chain id (node.go boots with
// DeriveNodeID(ids.Empty)). This is what makes Proposer() == the windower's
// elected NodeID under strict-PQ.
pub, err := mldsa.PublicKeyFromBytes(identity, mldsa.MLDSA65)
if err != nil {
return fmt.Errorf("%w: %w", errInvalidCertificate, err)
}
b.mldsaPub = pub
nodeID, _, err := ids.NodeIDSchemeMLDSA65.DeriveMLDSA(ids.Empty, identity)
if err != nil {
return err
}
b.proposer = nodeID
case schemeSecp256k1:
// Classical: identity is the DER TLS cert. NodeID = hash of the cert, the
// legacy upstream derivation used on non-strict-PQ chains.
b.cert, err = staking.ParseCertificate(identity)
if err != nil {
return fmt.Errorf("%w: %w", errInvalidCertificate, err)
}
b.proposer = ids.NodeIDFromCert(&ids.Certificate{
Raw: b.cert.Raw,
PublicKey: b.cert.PublicKey,
})
default:
return fmt.Errorf("%w: 0x%02x", errUnknownProposerScheme, b.scheme)
}
b.proposer = ids.NodeIDFromCert(&ids.Certificate{
Raw: b.cert.Raw,
PublicKey: b.cert.PublicKey,
})
return nil
}
func (b *statelessBlock) verify(chainID ids.ID) error {
if b.cert == nil {
if b.cert == nil && b.mldsaPub == nil {
if len(b.Signature) > 0 {
return errUnexpectedSignature
}
@@ -148,8 +199,18 @@ func (b *statelessBlock) verify(chainID ids.ID) error {
if err != nil {
return err
}
headerBytes := header.Bytes()
if b.mldsaPub != nil {
// FIPS 204 §5.2 domain-separated verification: the same proposervm context
// the signer bound, so a proposer signature can never be replayed as any
// other ML-DSA message (UTXO auth, consensus vote) the validator produces.
if !b.mldsaPub.VerifySignatureCtx(headerBytes, b.Signature, proposerSigCtx) {
return errMLDSAProposerSigInvalid
}
return nil
}
return staking.CheckSignature(
b.cert,
headerBytes,
@@ -178,6 +239,10 @@ func (b *statelessBlock) Proposer() ids.NodeID {
return b.proposer
}
func (b *statelessBlock) HasClassicalProposer() bool {
return b.scheme == schemeSecp256k1
}
func (b *statelessBlock) DARoot() [32]byte {
return read32(b.msg.Root(), offDARoot)
}
+67 -5
View File
@@ -9,10 +9,18 @@ import (
"time"
"github.com/luxfi/crypto/hash"
"github.com/luxfi/crypto/mldsa"
"github.com/luxfi/ids"
"github.com/luxfi/node/staking"
)
// proposerSigCtx domain-separates proposervm block signatures (FIPS 204 §5.2)
// from every other ML-DSA signature a validator makes (UTXO auth, consensus
// votes), so a proposer signature can never be replayed as another protocol's
// message. The classical (ECDSA) path gets its cross-chain separation from the
// chainID bound into the signed header.
var proposerSigCtx = []byte("lux-proposervm-block-v1")
func BuildUnsigned(
parentID ids.ID,
timestamp time.Time,
@@ -20,8 +28,10 @@ func BuildUnsigned(
epoch Epoch,
blockBytes []byte,
) (SignedBlock, error) {
// No certificate, no signature: the block bytes are just the unsigned body.
unsignedBytes := buildUnsignedBuffer(parentID, timestamp.Unix(), pChainHeight, epoch, nil, blockBytes)
// No identity, no signature: the block bytes are just the unsigned body. This
// is the pre-fork→post-fork transition block and every K=1 block (no proposer
// window), so small local nets never exercise a signed path.
unsignedBytes := buildUnsignedBuffer(parentID, timestamp.UnixMilli(), pChainHeight, epoch, nil, blockBytes)
block := &statelessBlock{}
if err := block.initialize(unsignedBytes); err != nil {
@@ -30,6 +40,9 @@ func BuildUnsigned(
return block, nil
}
// Build produces a CLASSICAL (secp256k1/ECDSA TLS-leaf) signed block: identity
// slot = [schemeSecp256k1 | cert DER], signature = ECDSA over SHA256(header).
// Used on chains whose validator set is keyed by classical NodeIDs.
func Build(
parentID ids.ID,
timestamp time.Time,
@@ -40,7 +53,57 @@ func Build(
chainID ids.ID,
key crypto.Signer,
) (SignedBlock, error) {
unsignedBytes := buildUnsignedBuffer(parentID, timestamp.Unix(), pChainHeight, epoch, cert.Raw, blockBytes)
return buildSigned(parentID, timestamp, pChainHeight, epoch,
schemeSecp256k1, cert.Raw, blockBytes, chainID,
func(headerBytes []byte) ([]byte, error) {
// ECDSA signs the digest; CheckSignature re-hashes the header to match.
return key.Sign(rand.Reader, hash.ComputeHash256(headerBytes), crypto.SHA256)
})
}
// BuildMLDSA produces a strict-PQ (ML-DSA-65, FIPS 204) signed block: identity
// slot = [schemeMLDSA65 | raw ML-DSA-65 pubkey], signature = ML-DSA over the raw
// header with the proposervm domain-separation context. The block's Proposer()
// NodeID derives via DeriveMLDSA, so it EQUALS the strict-PQ NodeID the windower
// elected — the fix for the height≥2 "unexpected proposer" verify failure.
func BuildMLDSA(
parentID ids.ID,
timestamp time.Time,
pChainHeight uint64,
epoch Epoch,
mldsaPub []byte,
blockBytes []byte,
chainID ids.ID,
key *mldsa.PrivateKey,
) (SignedBlock, error) {
return buildSigned(parentID, timestamp, pChainHeight, epoch,
schemeMLDSA65, mldsaPub, blockBytes, chainID,
func(headerBytes []byte) ([]byte, error) {
return key.SignCtx(rand.Reader, headerBytes, proposerSigCtx)
})
}
// buildSigned is the one signed-block core: it composes the [scheme|identity]
// proposer slot, encodes the unsigned body, derives the block ID, builds the
// (chainID, parentID, blockID) header, calls the scheme's signer over it, and
// appends the signature buffer. Build / BuildMLDSA differ ONLY in the scheme tag,
// the identity bytes, and the sign closure — DRY across primitives.
func buildSigned(
parentID ids.ID,
timestamp time.Time,
pChainHeight uint64,
epoch Epoch,
scheme byte,
identity []byte,
blockBytes []byte,
chainID ids.ID,
sign func(headerBytes []byte) ([]byte, error),
) (SignedBlock, error) {
idSlot := make([]byte, 0, 1+len(identity))
idSlot = append(idSlot, scheme)
idSlot = append(idSlot, identity...)
unsignedBytes := buildUnsignedBuffer(parentID, timestamp.UnixMilli(), pChainHeight, epoch, idSlot, blockBytes)
// The block ID is the hash of the unsigned prefix; the proposer signs a
// header binding (chainID, parentID, blockID).
@@ -51,8 +114,7 @@ func Build(
return nil, err
}
headerHash := hash.ComputeHash256(header.Bytes())
sig, err := key.Sign(rand.Reader, headerHash, crypto.SHA256)
sig, err := sign(header.Bytes())
if err != nil {
return nil, err
}
+321
View File
@@ -0,0 +1,321 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// build_inner_parent_test.go — the deterministic repro of the SELF-REJECTION LOOP
// measured on devnet 96367 and testnet 96368 on 2026-07-28:
//
// info built block blkID=2srokbs… height=1047 innerBlkID=1DMGHAoo… pChainHeight=0
// error built block failed verification — dropping
// error="inner parentID didn't match expected parent" height=1047 parentID=2V6mi4tX…
//
// 83456 drops/min per node, forever, with the externally visible tip frozen two heights
// BELOW what the builder kept proposing. Every node rejected the block it had just built.
//
// The invariant being violated is postForkCommonComponents.Verify's third check
// (block.go, errInnerParentMismatch): a child's inner parent MUST be the inner block
// wrapped by the outer parent it extends. buildChild did not establish it — it asked the
// inner VM to build, and the inner VM builds on ITS OWN head. These tests model the inner
// VM with the luxfi/evm head semantics that make the two pointers diverge, and assert the
// invariant on the block buildChild actually returns.
package proposervm
import (
"context"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/luxfi/database/memdb"
"github.com/luxfi/database/versiondb"
"github.com/luxfi/ids"
"github.com/luxfi/log"
validatorstest "github.com/luxfi/validators/validatorstest"
vmchain "github.com/luxfi/vm/chain"
"github.com/luxfi/vm/chain/blocktest"
"github.com/luxfi/node/vms/proposervm/block"
"github.com/luxfi/node/vms/proposervm/proposer"
"github.com/luxfi/node/vms/proposervm/state"
)
// evmLikeInner models the inner execution VM's HEAD, which is the only part of the inner
// VM this defect involves. Three behaviours are copied from luxfi/evm:
//
// - BuildBlock builds on the head (miner/worker.go: parent = chain.CurrentBlock())
// - verifyGossiped advances the head (core/blockchain.go writeBlockAndSetHead →
// newTip(block) → writeCanonicalBlockWithLogs → writeHeadBlock)
// - SetPreference reorgs the head, and is a no-op when it already matches
// (core/blockchain.go setPreference: early return on current.Hash() == block.Hash(),
// otherwise writeKnownBlock)
//
// Nothing else is stubbed: the test drives the REAL postForkBlock.buildChild.
type evmLikeInner struct {
vmchain.ChainVM
blocks map[ids.ID]*blocktest.Block // every block inserted into the inner chain
head ids.ID
setPrefCalls int
reorgs int
}
func newEVMLikeInner(genesis *blocktest.Block) *evmLikeInner {
return &evmLikeInner{
blocks: map[ids.ID]*blocktest.Block{genesis.IDV: genesis},
head: genesis.IDV,
}
}
func (c *evmLikeInner) insert(b *blocktest.Block) { c.blocks[b.IDV] = b }
// child mints a block extending [parent] without touching the head.
func (c *evmLikeInner) child(parent *blocktest.Block) *blocktest.Block {
id := ids.GenerateTestID()
b := &blocktest.Block{
IDV: id,
ParentV: parent.IDV,
HeightV: parent.HeightV + 1,
BytesV: id[:],
TimestampV: parent.TimestampV.Add(time.Second),
}
c.insert(b)
return b
}
// verifyGossiped is the drift mechanism: verifying a peer's block whose parent is the
// current head OPTIMISTICALLY makes it the head — no accept, no proposervm involvement.
func (c *evmLikeInner) verifyGossiped(b *blocktest.Block) {
c.insert(b)
if b.ParentV == c.head { // core/blockchain.go newTip
c.head = b.IDV
}
}
func (c *evmLikeInner) BuildBlock(context.Context) (vmchain.Block, error) {
return c.child(c.blocks[c.head]), nil
}
func (c *evmLikeInner) SetPreference(_ context.Context, id ids.ID) error {
c.setPrefCalls++
blk, ok := c.blocks[id]
if !ok {
return fmt.Errorf("evm-like inner VM: block %s not in chain", id)
}
if c.head == blk.IDV { // setPreference early return — already the head
return nil
}
c.head = blk.IDV
c.reorgs++
return nil
}
func (c *evmLikeInner) GetBlock(_ context.Context, id ids.ID) (vmchain.Block, error) {
blk, ok := c.blocks[id]
if !ok {
return nil, fmt.Errorf("evm-like inner VM: block %s not found", id)
}
return blk, nil
}
// anyoneCanProposeWindower makes every slot open, so buildChild takes the UNSIGNED path
// and needs no staking key. The proposer election is orthogonal to the parent invariant.
type anyoneCanProposeWindower struct{}
func (anyoneCanProposeWindower) Proposers(context.Context, uint64, uint64, int) ([]ids.NodeID, error) {
return nil, proposer.ErrAnyoneCanPropose
}
func (anyoneCanProposeWindower) Delay(context.Context, uint64, uint64, ids.NodeID, int) (time.Duration, error) {
return 0, proposer.ErrAnyoneCanPropose
}
func (anyoneCanProposeWindower) ExpectedProposer(context.Context, uint64, uint64, uint64) (ids.NodeID, error) {
return ids.EmptyNodeID, proposer.ErrAnyoneCanPropose
}
func (anyoneCanProposeWindower) MinDelayForProposer(context.Context, uint64, uint64, ids.NodeID, uint64) (time.Duration, error) {
return 0, proposer.ErrAnyoneCanPropose
}
// newBuildParentTestVM returns a proposervm whose outer PREFERRED envelope wraps
// innerParent, plus the inner chain, at the moment before a build attempt.
func newBuildParentTestVM(t *testing.T, innerParent *blocktest.Block, inner *evmLikeInner) (*VM, *postForkBlock) {
t.Helper()
vm := &VM{
verifiedBlocks: map[ids.ID]PostForkBlock{},
logger: log.Noop(),
}
vm.State = state.New(versiondb.New(memdb.New()))
vm.ChainVM = inner
vm.Windower = anyoneCanProposeWindower{}
vm.validatorState = &validatorstest.State{
GetCurrentHeightF: func(context.Context) (uint64, error) { return 0, nil },
}
// Deterministic clock, one second after the parent envelope: slot 0, so the child
// timestamp is not window-snapped and the parent check is the only thing under test.
parentTimestamp := innerParent.TimestampV
vm.Clock.Set(parentTimestamp.Add(time.Second))
statelessParent, err := block.BuildUnsigned(
ids.GenerateTestID(), // outer grandparent
parentTimestamp,
0, // pChainHeight — 0 fleet-wide, mainnet included; orthogonal to this defect
block.Epoch{},
innerParent.BytesV,
)
require.NoError(t, err)
return vm, &postForkBlock{
SignedBlock: statelessParent,
postForkCommonComponents: postForkCommonComponents{vm: vm, innerBlk: innerParent},
}
}
// TestBuildChild_InnerHeadDriftedByGossipVerify_ChildExtendsOuterParentsInner is the
// repro. The node's outer preferred wraps inner height 1045 (the accepted tip); it then
// VERIFIES a gossiped inner 1046, which optimistically becomes the EVM head. The next
// build must still produce 1046-off-1045 — the block its own Verify will accept.
//
// BEFORE THE FIX buildChild delegated straight to the inner VM, which built off the
// drifted head: a child at height 1047 whose inner parent is 1046, while the outer parent
// wraps 1045 ⇒ errInnerParentMismatch on the node's own block, dropped, repeat forever.
// That is the live devnet signature exactly: built 1047 against an accepted tip of 1045.
func TestBuildChild_InnerHeadDriftedByGossipVerify_ChildExtendsOuterParentsInner(t *testing.T) {
require := require.New(t)
ctx := context.Background()
genesis := &blocktest.Block{
IDV: ids.GenerateTestID(),
HeightV: 1044,
BytesV: []byte("1044"),
TimestampV: time.Unix(1_785_216_000, 0),
}
inner := newEVMLikeInner(genesis)
// The accepted inner tip, wrapped by this node's outer preferred envelope.
acceptedTip := inner.child(genesis) // height 1045
inner.head = acceptedTip.IDV
_, outerParent := newBuildParentTestVM(t, acceptedTip, inner)
// DRIFT: a gossiped inner block at 1046 verifies and optimistically takes the head.
gossiped := inner.child(acceptedTip) // height 1046
inner.verifyGossiped(gossiped)
require.Equal(gossiped.IDV, inner.head, "precondition: the EVM head drifted off the accepted tip")
built, err := outerParent.buildChild(ctx)
require.NoError(err)
child, ok := built.(*postForkBlock)
require.True(ok)
// THE INVARIANT — verbatim the condition postForkCommonComponents.Verify enforces
// (expectedInnerParentID := p.innerBlk.ID(); innerParentID := child.innerBlk.Parent()).
require.Equal(acceptedTip.ID(), child.innerBlk.Parent(),
"the built child's inner parent must be the outer parent's inner block, or the node "+
"rejects its own block with errInnerParentMismatch and drops it forever")
require.Equal(acceptedTip.HeightV+1, child.Height(),
"the child must be built one above the outer parent's inner block, not above the drifted head")
// And the head was reorged back — the drift is repaired, not merely tolerated.
require.Equal(acceptedTip.IDV, inner.head)
require.Equal(1, inner.reorgs)
}
// TestBuildChild_HealthyHead_NoReorg pins the no-behaviour-change half: when the inner
// head already IS the outer parent's inner block (every healthy node, every block), the
// re-anchor is a no-op inside the inner VM — no reorg, no head movement.
func TestBuildChild_HealthyHead_NoReorg(t *testing.T) {
require := require.New(t)
ctx := context.Background()
genesis := &blocktest.Block{
IDV: ids.GenerateTestID(),
HeightV: 100,
BytesV: []byte("100"),
TimestampV: time.Unix(1_785_216_000, 0),
}
inner := newEVMLikeInner(genesis)
acceptedTip := inner.child(genesis)
inner.head = acceptedTip.IDV
_, outerParent := newBuildParentTestVM(t, acceptedTip, inner)
built, err := outerParent.buildChild(ctx)
require.NoError(err)
require.Equal(acceptedTip.ID(), built.(*postForkBlock).innerBlk.Parent())
require.Equal(acceptedTip.IDV, inner.head, "head must not move on a healthy build")
require.Zero(inner.reorgs, "no reorg on a healthy node — the inner SetPreference early-returns")
}
// TestBuildChild_PreForkTransition_ChildExtendsThisBlock covers the OTHER build
// delegation, preForkBlock.buildChild, which produces the pre-fork→post-fork transition
// block and is verified by preForkBlock.verifyPostForkChild's identical inner-parent check
// ("Make sure [b] is the parent of [child]'s inner block", pre_fork_block.go).
//
// At the fork height every validator may emit its own unsigned transition candidate, so a
// gossiped sibling verifying here is the norm, not an edge case — and it drifts the inner
// head exactly as on a live chain. Without the anchor the transition wedges at chain
// start, which is every fresh net's first two blocks.
func TestBuildChild_PreForkTransition_ChildExtendsThisBlock(t *testing.T) {
require := require.New(t)
ctx := context.Background()
genesis := &blocktest.Block{
IDV: ids.GenerateTestID(),
HeightV: 0,
BytesV: []byte("genesis"),
TimestampV: time.Unix(1_785_216_000, 0),
}
inner := newEVMLikeInner(genesis)
vm, _ := newBuildParentTestVM(t, genesis, inner)
preFork := &preForkBlock{Block: genesis, vm: vm}
// A peer's competing transition candidate verifies and takes the inner head.
sibling := inner.child(genesis)
inner.verifyGossiped(sibling)
require.Equal(sibling.IDV, inner.head)
built, err := preFork.buildChild(ctx)
require.NoError(err)
require.Equal(genesis.ID(), built.(*postForkBlock).innerBlk.Parent(),
"the transition block's inner parent must be the pre-fork block itself")
require.Equal(genesis.HeightV+1, built.Height())
require.Equal(genesis.IDV, inner.head)
}
// TestBuildChild_InnerParentNotInInnerChain_FailsInsteadOfBuildingADoomedBlock covers the
// error case: if the inner VM cannot anchor on the outer parent's inner block, then the
// head is provably NOT that block, so any child built would fail this node's own Verify.
// Returning the error is strictly better than emitting a block we will drop.
func TestBuildChild_InnerParentNotInInnerChain_FailsInsteadOfBuildingADoomedBlock(t *testing.T) {
require := require.New(t)
ctx := context.Background()
genesis := &blocktest.Block{
IDV: ids.GenerateTestID(),
HeightV: 100,
BytesV: []byte("100"),
TimestampV: time.Unix(1_785_216_000, 0),
}
inner := newEVMLikeInner(genesis)
// An inner parent the inner VM does not hold (never inserted).
orphanID := ids.GenerateTestID()
orphan := &blocktest.Block{
IDV: orphanID,
ParentV: genesis.IDV,
HeightV: 101,
BytesV: orphanID[:],
TimestampV: genesis.TimestampV.Add(time.Second),
}
_, outerParent := newBuildParentTestVM(t, orphan, inner)
_, err := outerParent.buildChild(ctx)
require.ErrorContains(err, "failed to anchor inner build parent")
}
+17
View File
@@ -7,6 +7,7 @@ import (
"crypto"
"time"
"github.com/luxfi/crypto/mldsa"
"github.com/luxfi/ids"
"github.com/luxfi/metric"
@@ -34,6 +35,12 @@ type Config struct {
// Configurable minimal delay among blocks issued consecutively
MinBlkDelay time.Duration
// ProposerWindowDuration overrides the proposer-slot spacing (proposer.
// WindowDuration). Zero keeps the 5s mainnet default; small local/dev nets set
// it low (e.g. 1s or less) so block cadence is not floored at 5s per proposer
// slot. Applied once at VM init via proposer.SetWindowDuration.
ProposerWindowDuration time.Duration
// Maximal number of block indexed.
// Zero signals all blocks are indexed.
NumHistoricalBlocks uint64
@@ -44,6 +51,16 @@ type Config struct {
// Block certificate
StakingCertLeaf *staking.Certificate
// Strict-PQ block signer. When StakingMLDSASigner is non-nil the proposer
// signs post-fork blocks with ML-DSA-65 and stamps the block's proposer
// identity as the raw ML-DSA-65 public key, so Proposer() derives via
// DeriveMLDSA and EQUALS the strict-PQ NodeID the windower elected (the
// validator set is ML-DSA-keyed under strict-PQ). Nil ⇒ classical TLS-leaf
// signing above. Exactly one of the two schemes is active per chain, chosen at
// wiring time from the resolved ChainSecurityProfile.
StakingMLDSASigner *mldsa.PrivateKey
StakingMLDSAPub []byte
// Registerer for metric metrics
Registerer metric.Registerer
+302
View File
@@ -0,0 +1,302 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// height_backfill.go — RECOVERY for a proposervm finality index that sits BELOW
// the inner VM's accepted tip.
//
// THE INVARIANT. Every post-fork accept commits the outer envelope, its height
// index entry and the last-accepted pointer in ONE versiondb batch BEFORE the
// inner block is accepted (vm.acceptPostForkBlock → postForkBlock.Accept), so the
// proposervm index can only ever be AHEAD of the inner VM, never behind.
//
// HOW IT BREAKS ANYWAY. The proposervm has one accept path that advances the inner
// VM while leaving the outer index untouched: preForkBlock.Accept, whose
// acceptOuterBlk() is a no-op by construction. Post-fork that path was reachable
// because BOTH of the proposervm's block constructors fall back to it silently —
// ParseBlock() falls back to parsePreForkBlock when the outer envelope does not
// parse, and getBlock() falls back to getPreForkBlock when the id is not an outer
// id (which is exactly what the CANONICAL id recorded by the consensus ledger is:
// postForkCommonComponents.CanonicalID() == innerBlk.ID()). Either fallback hands
// the engine a preForkBlock wrapping a post-fork inner block; accepting it moves
// the inner VM and NOT the index. Nothing complained, because nothing asserted the
// invariant at the moment it was violated — it was only ever checked at the NEXT
// boot, by which point the node could not start. That is why the lag looked like
// a mysterious "persistence" problem: the writes were never issued at all.
//
// The prevention half lives at the two fallbacks + preForkBlock.acceptOuterBlk
// (see pre_fork_block.go and vm.getPreForkBlock): post-fork, a pre-fork block is
// never constructed and never accepted.
//
// THE RECOVERY half lives here, and it must work for nodes that are ALREADY
// damaged. It is two escalating steps, both OUTER-ONLY — they never re-execute,
// re-verify or re-accept an inner block, so a node whose EVM is already at the tip
// is repaired without touching the EVM:
//
// 1. rebuildOuterIndexFromStore: re-derive the height index and last-accepted
// pointer from the outer envelopes ALREADY in this node's block store, binding
// each candidate to the inner block this node itself accepted at that height.
// Fully local, no network, no operator. This is what turns "refuses to start"
// into "starts, repairs itself, and keeps its finality pointer".
//
// 2. If step 1 cannot reach the inner tip, the node still STARTS, in an explicit
// backfill-pending state: loud, fail-safe (it will not BUILD a block while its
// finality index is incomplete) and repairable through BackfillOuterBlock,
// which accepts the missing envelopes from any source and applies the SAME
// binding checks. The alternative — the previous behaviour — was a dead
// C-Chain and a destructive resync.
//
// WHAT IS NEVER DONE, and why: the finality pointer is never dropped
// (DeleteLastAccepted). proposervm.LastAccepted would then fall back to an
// INNER-namespace id whose ParentID is contiguity-incompatible with the network's
// OUTER wrappers, permanently wedging bootstrap, catch-up and live Verify at the
// inner tip — a silent wedge strictly worse than the loud stop it would replace.
// Every path below only ever moves the pointer FORWARD, onto an envelope that was
// proven to wrap the inner block this node already accepted at that height.
package proposervm
import (
"bytes"
"context"
"crypto/sha256"
"errors"
"fmt"
"github.com/luxfi/ids"
"github.com/luxfi/log"
statelessblock "github.com/luxfi/node/vms/proposervm/block"
"github.com/luxfi/node/vms/proposervm/state"
)
var (
// ErrOuterBackfillNotPending is returned by BackfillOuterBlock when the index
// is already whole — the repair is idempotent, not an error to call twice.
ErrOuterBackfillNotPending = errors.New("proposervm: no outer-index backfill is pending")
// ErrOuterBackfillRejected is returned when a candidate envelope does not bind
// to this node's own accepted chain. Fail-closed: an envelope is only indexed
// when it provably wraps the inner block WE accepted at that height and chains
// off the envelope WE already hold at height-1, so a peer (or a stale file)
// cannot steer the finality pointer.
ErrOuterBackfillRejected = errors.New("proposervm: outer backfill candidate rejected")
// errPreForkAfterFork is the prevention guard: post-fork, a block at or above
// the recorded fork height is NEVER a pre-fork block. Constructing or accepting
// one is what silently strands the finality index below the inner tip.
errPreForkAfterFork = errors.New("proposervm: refusing a pre-fork block at or above the fork height")
)
// outerBackfill is the state of an incomplete outer index. Zero value = whole.
//
// It is guarded by its own leaf mutex (vm.backfillMu) rather than vm.lock: the
// accept path, the build path and the operator repair path all read it, and it is
// never held across a callout, so it cannot participate in a deadlock.
type outerBackfill struct {
// next is the FIRST height whose outer envelope is missing. It advances as
// envelopes land, so it doubles as the resume point across restarts.
next uint64
// tip is the inner VM's accepted height — the height the index must reach.
tip uint64
// innerTipID is the inner block at [tip], recorded for diagnostics.
innerTipID ids.ID
}
// NeedsOuterBackfill reports the still-missing outer height range [from, to] and
// whether a backfill is pending. It is the seam an operator tool or a node-layer
// fetch driver reads; when pending is false the index is whole.
func (vm *VM) NeedsOuterBackfill() (from uint64, to uint64, pending bool) {
vm.backfillMu.RLock()
defer vm.backfillMu.RUnlock()
if vm.backfill == nil {
return 0, 0, false
}
return vm.backfill.next, vm.backfill.tip, true
}
// outerBackfillPending is the internal fast check used by the build gate.
func (vm *VM) outerBackfillPending() bool {
vm.backfillMu.RLock()
defer vm.backfillMu.RUnlock()
return vm.backfill != nil
}
// innerBlockAtHeight returns the inner VM's ACCEPTED block at [height] together
// with its bytes — the binding target every repair candidate must match. It reads
// the inner VM's own height index, so it is the inner VM's truth, not ours.
func (vm *VM) innerBlockAtHeight(ctx context.Context, height uint64) (ids.ID, []byte, error) {
innerID, err := vm.ChainVM.GetBlockIDAtHeight(ctx, height)
if err != nil {
return ids.Empty, nil, err
}
innerBlk, err := vm.ChainVM.GetBlock(ctx, innerID)
if err != nil {
return ids.Empty, nil, err
}
return innerID, innerBlk.Bytes(), nil
}
// indexOuterAtHeight commits ONE outer envelope into the finality index: the
// envelope itself, its height entry, and the last-accepted pointer, in a single
// versiondb batch — byte-for-byte what acceptPostForkBlock writes on the live
// path. It deliberately does NOT touch the inner VM.
func (vm *VM) indexOuterAtHeight(height uint64, blk statelessblock.Block) error {
if err := vm.State.PutBlock(blk); err != nil {
return err
}
if err := vm.State.SetBlockIDAtHeight(height, blk.ID()); err != nil {
return err
}
if err := vm.State.SetLastAccepted(blk.ID()); err != nil {
return err
}
return vm.db.Commit()
}
// rebuildOuterIndexFromStore walks the gap (proHeight, innerHeight] forward,
// re-indexing each height from an outer envelope ALREADY present in this node's
// block store.
//
// Soundness. A candidate at height h is accepted ONLY if
//
// envelope.Block() == the bytes of the inner block WE accepted at h, and
// envelope.ParentID() == the envelope we just indexed at h-1
//
// so the rebuilt chain is the unique outer chain that wraps our own accepted inner
// chain. Nothing is inferred and nothing is fabricated: an unmatched height stops
// the walk immediately.
//
// Returns the height the index reached (>= proHeight). A return equal to
// innerHeight means the index is whole again.
func (vm *VM) rebuildOuterIndexFromStore(
ctx context.Context,
proHeight uint64,
proID ids.ID,
innerHeight uint64,
) (uint64, error) {
// One pass over the block store: inner-bytes digest -> outer envelope. Keyed by
// digest rather than by inner id because deriving an inner id from bytes would
// mean parsing untrusted bytes through the inner VM; the digest comparison is
// exact and side-effect free.
byInnerDigest := make(map[[sha256.Size]byte]statelessblock.Block)
if err := state.ScanBlocks(vm.db, func(blk statelessblock.Block) error {
byInnerDigest[sha256.Sum256(blk.Block())] = blk
return nil
}); err != nil {
return proHeight, fmt.Errorf("failed to scan the proposervm block store: %w", err)
}
reached := proHeight
parentID := proID
for h := proHeight + 1; h <= innerHeight; h++ {
_, innerBytes, err := vm.innerBlockAtHeight(ctx, h)
if err != nil {
vm.logger.Warn("proposervm outer-index rebuild: inner block unreadable, stopping walk",
log.Uint64("height", h), log.Err(err))
break
}
candidate, ok := byInnerDigest[sha256.Sum256(innerBytes)]
if !ok || candidate.ParentID() != parentID {
break
}
if err := vm.indexOuterAtHeight(h, candidate); err != nil {
return reached, fmt.Errorf("failed to re-index outer block at height %d: %w", h, err)
}
reached = h
parentID = candidate.ID()
}
return reached, nil
}
// enterOuterBackfill records an incomplete index and lets the VM start anyway.
// Fail-safe, not fail-open: BuildBlock is gated off while pending (a node whose
// finality index has a hole must never PROPOSE), the height index still reports
// ErrNotFound inside the gap so nothing reads a wrong id, and the last-accepted
// pointer is left exactly where the rebuild got to.
func (vm *VM) enterOuterBackfill(from, tip uint64, innerTipID ids.ID) {
vm.backfillMu.Lock()
vm.backfill = &outerBackfill{next: from, tip: tip, innerTipID: innerTipID}
vm.backfillMu.Unlock()
vm.logger.Warn("proposervm STARTING WITH AN INCOMPLETE FINALITY INDEX — outer backfill pending",
log.Uint64("firstMissingHeight", from),
log.Uint64("innerTipHeight", tip),
log.Stringer("innerTipID", innerTipID),
log.String("effect", "this chain will NOT build blocks until the missing outer envelopes are supplied"),
log.String("recovery", "feed the outer proposervm envelopes for the missing heights via "+
"BackfillOuterBlock (cmd/repair-proposervm backfill) — each is bound to the inner block "+
"this node already accepted, so no EVM re-execution and no resync are required"),
)
}
// BackfillOuterBlock supplies ONE missing outer envelope, oldest-first, and is the
// operator/driver half of the recovery. It is fail-closed and idempotent:
//
// - not pending -> ErrOuterBackfillNotPending
// - envelope below the next height -> nil (already indexed; tolerate replay)
// - signature/format invalid -> error (parsed WITH verification)
// - not the next height, wrong parent,
// or wrapping a different inner block-> ErrOuterBackfillRejected
//
// On the final height it clears the pending state and refreshes the in-memory
// last-accepted metadata, after which the chain behaves exactly like one that
// booted whole.
func (vm *VM) BackfillOuterBlock(ctx context.Context, outerBytes []byte) error {
vm.backfillMu.Lock()
defer vm.backfillMu.Unlock()
if vm.backfill == nil {
return ErrOuterBackfillNotPending
}
height := vm.backfill.next
// Parse WITH proposer-signature verification: the envelope must be a real
// block of THIS chain, not merely well-formed bytes.
blk, err := statelessblock.Parse(outerBytes, vm.rt.ChainID)
if err != nil {
return fmt.Errorf("%w: envelope did not parse/verify: %w", ErrOuterBackfillRejected, err)
}
// BIND 1 — the envelope must chain off the one we already hold.
parentID, err := vm.State.GetLastAccepted()
if err != nil {
return fmt.Errorf("%w: cannot read the current finality pointer: %w", ErrOuterBackfillRejected, err)
}
if blk.ParentID() != parentID {
return fmt.Errorf("%w: envelope parent %s != current finality pointer %s (feed oldest-first)",
ErrOuterBackfillRejected, blk.ParentID(), parentID)
}
// BIND 2 — the envelope must wrap the inner block WE accepted at this height.
// This is what makes accepting bytes from an untrusted source safe.
innerID, innerBytes, err := vm.innerBlockAtHeight(ctx, height)
if err != nil {
return fmt.Errorf("%w: inner block at height %d unreadable: %w", ErrOuterBackfillRejected, height, err)
}
if !bytes.Equal(blk.Block(), innerBytes) {
return fmt.Errorf("%w: envelope at height %d does not wrap our accepted inner block %s",
ErrOuterBackfillRejected, height, innerID)
}
if err := vm.indexOuterAtHeight(height, blk); err != nil {
return err
}
vm.logger.Info("proposervm outer backfill: height repaired",
log.Uint64("height", height),
log.Stringer("outerID", blk.ID()),
log.Uint64("remaining", vm.backfill.tip-height),
)
if height >= vm.backfill.tip {
tip := vm.backfill.tip
vm.backfill = nil
if err := vm.setLastAcceptedMetadata(ctx); err != nil {
return fmt.Errorf("outer backfill completed at height %d but last-accepted metadata failed: %w", tip, err)
}
vm.logger.Info("proposervm FINALITY INDEX FULLY REPAIRED — backfill complete",
log.Uint64("height", tip))
return nil
}
vm.backfill.next = height + 1
return nil
}
+253
View File
@@ -0,0 +1,253 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// height_backfill_test.go — RECOVERY: a node whose proposervm finality index is
// ALREADY behind the inner VM tip must BOOT and REPAIR itself instead of killing
// the chain ("non-critical chain failed to initialize chainAlias=C" — a dead
// C-Chain on a pod that still reports 1/1 Ready).
//
// Two paths, both OUTER-ONLY (no inner re-execution, no EVM rollback, no resync):
// - the envelopes for the gap are still in the local block store -> rebuilt at
// boot with no network and no operator;
// - they are not -> the chain STARTS anyway, loud, build-gated, and heals from
// supplied envelopes through BackfillOuterBlock.
//
// The shared harness lives in height_lag_repro_test.go.
package proposervm
import (
"context"
"errors"
"testing"
"time"
"github.com/luxfi/ids"
statelessblock "github.com/luxfi/node/vms/proposervm/block"
)
// ---------------------------------------------------------------------------
// 2. RECOVERY — a node whose index is ALREADY behind boots and heals.
// ---------------------------------------------------------------------------
// TestOuterIndexRebuild_FromLocalStore_BootsAndHeals: the damaged node still holds
// the outer envelopes for the gap in its own block store (the truncated-index /
// inconsistently-restored-snapshot case). Boot must rebuild the index from them —
// no network, no operator, no resync — and must NOT return an error.
func TestOuterIndexRebuild_FromLocalStore_BootsAndHeals(t *testing.T) {
ctx := context.Background()
ic := newInnerChain(t, 8)
vm := testVM(t, ic)
outers := acceptThroughProposervm(t, vm, ic, 8)
// DAMAGE: roll the finality pointer + height index back to 5 while the inner VM
// stays at 8 — byte-for-byte the on-disk state luxd-3 booted with (index 6,
// inner 98), except the envelopes survive in the block store.
if err := vm.State.SetLastAccepted(outers[5].ID()); err != nil {
t.Fatalf("SetLastAccepted: %v", err)
}
for h := uint64(6); h <= 8; h++ {
if err := vm.State.DeleteBlockIDAtHeight(h); err != nil {
t.Fatalf("DeleteBlockIDAtHeight(%d): %v", h, err)
}
}
if err := vm.db.Commit(); err != nil {
t.Fatalf("commit damage: %v", err)
}
if got := indexHeight(t, vm); got != 5 {
t.Fatalf("precondition: damaged index must read 5, got %d", got)
}
// BOOT.
if err := vm.repairAcceptedChainByHeight(ctx); err != nil {
t.Fatalf("boot with a behind index must REPAIR, not fail: %v", err)
}
if _, _, pending := vm.NeedsOuterBackfill(); pending {
t.Fatal("the local store held every missing envelope — no backfill should be pending")
}
if got := indexHeight(t, vm); got != 8 {
t.Fatalf("index must be rebuilt to the inner tip 8, got %d", got)
}
lastAccepted, err := vm.State.GetLastAccepted()
if err != nil || lastAccepted != outers[8].ID() {
t.Fatalf("finality pointer must be the canonical envelope at 8 (%s), got %s (err %v)",
outers[8].ID(), lastAccepted, err)
}
for h := uint64(6); h <= 8; h++ {
got, err := vm.State.GetBlockIDAtHeight(h)
if err != nil || got != outers[h].ID() {
t.Fatalf("height index at %d must be %s, got %s (err %v)", h, outers[h].ID(), got, err)
}
}
}
// TestOuterBackfill_HealsFromSuppliedEnvelopes: the envelopes for the gap are NOT
// in the local store (the pre-fork-fallback case — they were never persisted).
// The node must still START — the old code killed the C-Chain here — come up
// explicitly backfill-pending, refuse to build, and then heal from supplied
// envelopes with no EVM re-execution.
func TestOuterBackfill_HealsFromSuppliedEnvelopes(t *testing.T) {
ctx := context.Background()
ic := newInnerChain(t, 8)
vm := testVM(t, ic)
acceptThroughProposervm(t, vm, ic, 5)
lastGood, err := vm.State.GetLastAccepted()
if err != nil {
t.Fatalf("GetLastAccepted: %v", err)
}
// DAMAGE: the inner VM ran on to 8 without the index (exactly what the pre-fork
// fallback did in production). No envelopes for 6..8 exist locally.
ic.accept(8)
// BOOT — must not error.
if err := vm.repairAcceptedChainByHeight(ctx); err != nil {
t.Fatalf("boot with a behind index must START the chain, got: %v", err)
}
from, to, pending := vm.NeedsOuterBackfill()
if !pending || from != 6 || to != 8 {
t.Fatalf("want backfill pending 6..8, got from=%d to=%d pending=%v", from, to, pending)
}
// FAIL-SAFE: a node with a hole in its finality index must not propose.
if _, err := vm.BuildBlock(ctx); err == nil {
t.Fatal("BuildBlock must refuse while the finality index is incomplete")
}
// NEGATIVE 1 — an envelope wrapping a DIFFERENT inner block than the one we
// accepted at this height is rejected (a peer cannot steer the pointer).
bogus, err := statelessblock.BuildUnsigned(
lastGood, time.Unix(6, 0), 0, statelessblock.Epoch{}, []byte("not-our-inner-block"))
if err != nil {
t.Fatalf("BuildUnsigned: %v", err)
}
if err := vm.BackfillOuterBlock(ctx, bogus.Bytes()); !errors.Is(err, ErrOuterBackfillRejected) {
t.Fatalf("an envelope that does not wrap our accepted inner block must be rejected, got %v", err)
}
// NEGATIVE 2 — out of order (height 7 before 6) is rejected: it does not chain
// off the current finality pointer.
outOfOrder := outerFor(t, ic, 7, lastGood)
if err := vm.BackfillOuterBlock(ctx, outOfOrder.Bytes()); !errors.Is(err, ErrOuterBackfillRejected) {
t.Fatalf("an out-of-order envelope must be rejected, got %v", err)
}
// HEAL — feed 6, 7, 8 oldest-first, as a healthy peer or the repair tool would.
parent := lastGood
want := map[uint64]ids.ID{}
for h := uint64(6); h <= 8; h++ {
sb := outerFor(t, ic, h, parent)
if err := vm.BackfillOuterBlock(ctx, sb.Bytes()); err != nil {
t.Fatalf("BackfillOuterBlock(h=%d): %v", h, err)
}
want[h] = sb.ID()
parent = sb.ID()
}
if _, _, pending := vm.NeedsOuterBackfill(); pending {
t.Fatal("backfill must be complete after the last height")
}
if got := indexHeight(t, vm); got != 8 {
t.Fatalf("index must reach the inner tip 8, got %d", got)
}
for h := uint64(6); h <= 8; h++ {
got, err := vm.State.GetBlockIDAtHeight(h)
if err != nil || got != want[h] {
t.Fatalf("height index at %d must be %s, got %s (err %v)", h, want[h], got, err)
}
}
// Idempotent: nothing pending, so a replayed call says so rather than corrupting.
if err := vm.BackfillOuterBlock(ctx, outOfOrder.Bytes()); !errors.Is(err, ErrOuterBackfillNotPending) {
t.Fatalf("want ErrOuterBackfillNotPending after completion, got %v", err)
}
// The chain is a full participant again.
if _, _, pending := vm.NeedsOuterBackfill(); pending {
t.Fatal("build gate must have lifted")
}
// And a fresh boot on the repaired state is a clean no-op.
if err := vm.repairAcceptedChainByHeight(ctx); err != nil {
t.Fatalf("boot after repair must be a no-op, got %v", err)
}
}
// TestBootWithBehindIndex_NeverKillsTheChain is the direct regression lock on the
// production symptom: repairAcceptedChainByHeight must NEVER return an error for a
// behind index, because chains/manager.go turns that error into
// "non-critical chain failed to initialize chainAlias=C" — a dead C-Chain on a pod
// that still reports 1/1 Ready.
func TestBootWithBehindIndex_NeverKillsTheChain(t *testing.T) {
ctx := context.Background()
for _, gap := range []uint64{1, 2, 6, 91} {
ic := newInnerChain(t, 100)
vm := testVM(t, ic)
acceptThroughProposervm(t, vm, ic, 100-gap)
ic.accept(100)
if err := vm.repairAcceptedChainByHeight(ctx); err != nil {
t.Fatalf("gap %d: boot must not fail, got %v", gap, err)
}
from, to, pending := vm.NeedsOuterBackfill()
if !pending || from != 100-gap+1 || to != 100 {
t.Fatalf("gap %d: want pending %d..100, got %d..%d pending=%v", gap, 100-gap+1, from, to, pending)
}
}
}
func mustForkHeight(t *testing.T, vm *VM) uint64 {
t.Helper()
h, err := vm.State.GetForkHeight()
if err != nil {
t.Fatalf("GetForkHeight: %v", err)
}
return h
}
// TestOuterBackfill_SelfHealsFromOrdinaryParseTraffic proves the loop closes with
// no new transport and no operator: every envelope a peer serves passes through
// VM.ParseBlock, and a backfill-pending node absorbs exactly the ones it needs.
// This is what lets an ALREADY-DAMAGED node come back by itself.
func TestOuterBackfill_SelfHealsFromOrdinaryParseTraffic(t *testing.T) {
ctx := context.Background()
ic := newInnerChain(t, 8)
vm := testVM(t, ic)
acceptThroughProposervm(t, vm, ic, 5)
lastGood, err := vm.State.GetLastAccepted()
if err != nil {
t.Fatalf("GetLastAccepted: %v", err)
}
ic.accept(8) // the inner VM ran on without the index
if err := vm.repairAcceptedChainByHeight(ctx); err != nil {
t.Fatalf("boot must start the chain, got %v", err)
}
if _, _, pending := vm.NeedsOuterBackfill(); !pending {
t.Fatal("precondition: backfill must be pending")
}
// Peer traffic, oldest-first, delivered through the ORDINARY parse path.
parent := lastGood
for h := uint64(6); h <= 8; h++ {
sb := outerFor(t, ic, h, parent)
if _, err := vm.ParseBlock(ctx, sb.Bytes()); err != nil {
t.Fatalf("ParseBlock(h=%d): %v", h, err)
}
parent = sb.ID()
}
if _, _, pending := vm.NeedsOuterBackfill(); pending {
t.Fatal("the node must have healed itself from ordinary parse traffic")
}
if got := indexHeight(t, vm); got != 8 {
t.Fatalf("index must reach the inner tip 8, got %d", got)
}
// The build gate lifted with the heal (the gate itself is asserted in
// TestOuterBackfill_HealsFromSuppliedEnvelopes; driving the real builder here
// would need a validator set this harness deliberately does not stand up).
if _, _, pending := vm.NeedsOuterBackfill(); pending {
t.Fatal("build gate must have lifted")
}
}
+280
View File
@@ -0,0 +1,280 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// height_lag_repro_test.go — the DETERMINISTIC REPRODUCTION of the proposervm
// finality-index lag, written against the PRE-EXISTING proposervm API only (no
// symbol introduced by the fix), so it fails on the unfixed code and passes on the
// fixed code without being tautological.
//
// The production failure it reproduces (lux-devnet luxd-3, C-Chain, verbatim):
//
// VM initialization failed error="failed to repair accepted chain by height:
// proposervm finality index (height 6, id ns5qGN4i…) is BEHIND the inner VM tip
// (height 98, id TUTR74eA…)" chainID=21HieZng…
// non-critical chain failed to initialize … chainAlias=C
//
// THE MECHANISM. Every post-fork accept commits the envelope, its height index
// entry and the last-accepted pointer in ONE versiondb batch BEFORE the inner
// block is accepted, so the index can only run AHEAD. But the proposervm has one
// accept path that moves the inner VM and writes NOTHING: preForkBlock.Accept,
// whose acceptOuterBlk() is a no-op. Post-fork it was reachable because getBlock()
// silently falls back to constructing a preForkBlock whenever the id is not an
// OUTER envelope id — and the id this fork's consensus ledger records as canonical
// IS the inner block's id (postForkCommonComponents.CanonicalID). One such accept
// and the index is stranded: invisible while running, fatal at the next boot.
//
// This file also holds the shared harness the recovery tests use.
package proposervm
import (
"context"
"errors"
"testing"
"time"
"github.com/luxfi/database"
"github.com/luxfi/database/memdb"
"github.com/luxfi/database/versiondb"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/metric"
"github.com/luxfi/runtime"
vmchain "github.com/luxfi/vm/chain"
"github.com/luxfi/vm/chain/blocktest"
"github.com/luxfi/node/cache/lru"
statelessblock "github.com/luxfi/node/vms/proposervm/block"
"github.com/luxfi/node/vms/proposervm/state"
"github.com/luxfi/node/vms/proposervm/tree"
)
// innerChain is a stand-in for the inner execution VM (the C-Chain EVM): a
// contiguous accepted chain 1..tip answering LastAccepted / GetBlock /
// GetBlockIDAtHeight / ParseBlock exactly as the real one does.
type innerChain struct {
byHeight map[uint64]*blocktest.Block
byID map[ids.ID]*blocktest.Block
byBytes map[string]*blocktest.Block
tip uint64
}
func newInnerChain(t *testing.T, tip uint64) *innerChain {
t.Helper()
ic := &innerChain{
byHeight: map[uint64]*blocktest.Block{},
byID: map[ids.ID]*blocktest.Block{},
byBytes: map[string]*blocktest.Block{},
}
parent := ids.Empty
for h := uint64(1); h <= tip; h++ {
id := ids.GenerateTestID()
blk := &blocktest.Block{
IDV: id,
ParentV: parent,
HeightV: h,
BytesV: append([]byte("inner"), id[:]...),
TimestampV: time.Unix(int64(h), 0),
}
ic.byHeight[h] = blk
ic.byID[id] = blk
ic.byBytes[string(blk.BytesV)] = blk
parent = id
}
ic.tip = tip
return ic
}
// accept models the inner VM accepting a block: its head advances. This is the
// ONLY way the inner tip moves, exactly as in the real EVM.
func (ic *innerChain) accept(h uint64) { ic.tip = h }
func (ic *innerChain) vm() *blocktest.VM {
return &blocktest.VM{
LastAcceptedF: func(context.Context) (ids.ID, error) {
return ic.byHeight[ic.tip].IDV, nil
},
GetBlockF: func(_ context.Context, id ids.ID) (vmchain.Block, error) {
blk, ok := ic.byID[id]
if !ok {
return nil, errors.New("inner: block not found")
}
return blk, nil
},
GetBlockIDAtHeightF: func(_ context.Context, h uint64) (ids.ID, error) {
blk, ok := ic.byHeight[h]
if !ok {
return ids.Empty, errors.New("inner: height not indexed")
}
return blk.IDV, nil
},
ParseBlockF: func(_ context.Context, b []byte) (vmchain.Block, error) {
blk, ok := ic.byBytes[string(b)]
if !ok {
return nil, errors.New("inner: unparseable bytes")
}
return blk, nil
},
}
}
// testVM builds a proposervm over memdb wrapping [ic] — the real VM struct with
// the real State, so every write below goes through the production code paths.
func testVM(t *testing.T, ic *innerChain) *VM {
t.Helper()
return testVMOnBase(t, ic, memdb.New())
}
// testVMOnBase is testVM over a caller-supplied base database — the seam the
// crash-boot tests (vm_crashboot_test.go) use to boot a SECOND, cold VM over
// bytes copied out of a running one.
func testVMOnBase(t *testing.T, ic *innerChain, base database.Database) *VM {
t.Helper()
db := versiondb.New(base)
metrics := metric.New("")
vm := &VM{
ChainVM: ic.vm(),
db: db,
State: state.New(db),
Tree: tree.New(),
verifiedBlocks: map[ids.ID]PostForkBlock{},
innerBlkCache: lru.NewSizedCache(innerBlkCacheSize, cachedBlockSize),
logger: log.Noop(),
rt: &runtime.Runtime{ChainID: ids.GenerateTestID()},
}
vm.lastAcceptedTimestampGaugeVec = metrics.NewGaugeVec(
"last_accepted_timestamp", "timestamp of the last block accepted", []string{"block_type"})
return vm
}
// outerFor wraps the inner block at [h] in a real proposervm envelope chained off
// [parentOuter] — the same shape a proposer builds.
func outerFor(t *testing.T, ic *innerChain, h uint64, parentOuter ids.ID) statelessblock.SignedBlock {
t.Helper()
sb, err := statelessblock.BuildUnsigned(
parentOuter, time.Unix(int64(h), 0), 0, statelessblock.Epoch{}, ic.byHeight[h].BytesV)
if err != nil {
t.Fatalf("BuildUnsigned(h=%d): %v", h, err)
}
return sb
}
// acceptThroughProposervm drives heights 1..through through the REAL post-fork
// accept path, so index and inner head advance in lock-step. This is a healthy
// node, and it is what records the fork height.
func acceptThroughProposervm(t *testing.T, vm *VM, ic *innerChain, through uint64) map[uint64]statelessblock.SignedBlock {
t.Helper()
return acceptRangeThroughProposervm(t, vm, ic, 1, through, ids.Empty)
}
// acceptRangeThroughProposervm is acceptThroughProposervm for heights
// from..through, chaining the first envelope off parentOuter — so a test can
// keep a source node accepting AFTER a crash copy was taken.
func acceptRangeThroughProposervm(t *testing.T, vm *VM, ic *innerChain, from, through uint64, parentOuter ids.ID) map[uint64]statelessblock.SignedBlock {
t.Helper()
outers := map[uint64]statelessblock.SignedBlock{}
for h := from; h <= through; h++ {
sb := outerFor(t, ic, h, parentOuter)
blk := &postForkBlock{
SignedBlock: sb,
postForkCommonComponents: postForkCommonComponents{vm: vm, innerBlk: ic.byHeight[h]},
}
vm.Tree.Add(ic.byHeight[h])
if err := blk.Accept(context.Background()); err != nil {
t.Fatalf("post-fork Accept(h=%d): %v", h, err)
}
ic.accept(h)
outers[h] = sb
parentOuter = sb.ID()
}
return outers
}
// indexHeight reads the height the PERSISTED finality index sits at — the exact
// quantity repairAcceptedChainByHeight compares against the inner tip at boot.
func indexHeight(t *testing.T, vm *VM) uint64 {
t.Helper()
id, err := vm.State.GetLastAccepted()
if err != nil {
t.Fatalf("GetLastAccepted: %v", err)
}
blk, err := vm.getPostForkBlock(context.Background(), id)
if err != nil {
t.Fatalf("getPostForkBlock(%s): %v", id, err)
}
return blk.Height()
}
// TestFinalityIndexLag_IndexMustNeverFallBehindInnerTip is THE reproduction.
//
// It asks the proposervm for the block at the CANONICAL id of height 6 — the id
// the consensus ledger records for a proposervm-wrapped block — accepts whatever
// comes back, and then asserts the two things a correct proposervm guarantees:
//
// 1. the persisted finality index is never below the inner VM tip, and
// 2. a boot-time reconciliation of that state never kills the chain.
//
// UNFIXED, getBlock hands back a preForkBlock, its Accept is a silent no-op on the
// outer index, the inner VM advances to 6, and this test fails on (1) — printing
// the same divergence luxd-3 died of. FIXED, the pre-fork construction/accept is
// refused, nothing diverges, and both assertions hold.
//
// It uses NO symbol introduced by the fix, so it is a genuine before/after probe.
func TestFinalityIndexLag_IndexMustNeverFallBehindInnerTip(t *testing.T) {
ctx := context.Background()
ic := newInnerChain(t, 8)
vm := testVM(t, ic)
acceptThroughProposervm(t, vm, ic, 5)
if got, tip := indexHeight(t, vm), ic.tip; got != 5 || tip != 5 {
t.Fatalf("precondition: want index=5 inner=5, got index=%d inner=%d", got, tip)
}
forkHeight, err := vm.State.GetForkHeight()
if err != nil {
t.Fatalf("GetForkHeight: %v", err)
}
// The engine asks for height 6 by its CANONICAL (inner) id.
if blk, err := vm.getBlock(ctx, ic.byHeight[6].IDV); err == nil {
if _, isPreFork := blk.(*preForkBlock); isPreFork {
t.Logf("getBlock(canonical id @6) returned a preForkBlock — fork height is %d", forkHeight)
}
if err := blk.Accept(ctx); err == nil {
ic.accept(6) // the inner VM applied it
}
}
// ASSERTION 1 — the invariant.
if got := indexHeight(t, vm); got < ic.tip {
// Errorf, not Fatalf: assertion 2 below must also run, so one failing run
// shows the WHOLE production symptom chain (lag now -> dead chain at boot).
t.Errorf("FINALITY INDEX LAG REPRODUCED: index is at height %d but the inner VM tip is %d "+
"(fork height %d). An accept advanced the inner VM without writing the proposervm index; "+
"this node is now unbootable", got, ic.tip, forkHeight)
}
// ASSERTION 2 — boot must not kill the chain.
if err := vm.repairAcceptedChainByHeight(ctx); err != nil {
t.Fatalf("BOOT KILLED THE CHAIN: %v", err)
}
}
// TestPreForkAcceptStillWorksBeforeFork is the positive control for the guard: on
// a chain that has NOT forked there is no fork height, every block is legitimately
// pre-fork, and both construction and Accept must still work. A guard that broke
// this would brick every chain before its transition block.
func TestPreForkAcceptStillWorksBeforeFork(t *testing.T) {
ctx := context.Background()
ic := newInnerChain(t, 3)
vm := testVM(t, ic)
if _, err := vm.State.GetForkHeight(); err == nil {
t.Fatal("precondition: a fresh chain must have no fork height")
}
pre := &preForkBlock{Block: ic.byHeight[1], vm: vm}
if err := pre.Accept(ctx); err != nil {
t.Fatalf("pre-fork accept before the fork must succeed, got %v", err)
}
if _, err := vm.getPreForkBlock(ctx, ic.byHeight[1].IDV); err != nil {
t.Fatalf("pre-fork getBlock before the fork must succeed, got %v", err)
}
}
+25 -3
View File
@@ -65,8 +65,20 @@ func (b *preForkBlock) Accept(ctx context.Context) error {
return b.acceptInnerBlk(ctx)
}
func (*preForkBlock) acceptOuterBlk() error {
return nil
// acceptOuterBlk is a NO-OP for a genuine pre-fork block — a pre-fork block has no
// outer envelope, so there is nothing to index — and a hard REFUSAL once the chain
// has forked.
//
// The refusal is the second half of the finality-index root-cause fix (the first is
// vm.getPreForkBlock). Accepting a pre-fork block at or above the fork height
// advances the inner VM while leaving the proposervm index exactly where it was:
// the index silently falls behind the inner tip during normal operation, and the
// NEXT boot cannot start the chain. Refusing here converts that invisible,
// deferred, fatal corruption into an immediate, attributable error at the exact
// accept that would have caused it — and the engine's accept path is fail-closed,
// so finality stops rather than diverging.
func (b *preForkBlock) acceptOuterBlk() error {
return b.vm.refusePreForkAfterFork(b.Height())
}
func (b *preForkBlock) acceptInnerBlk(ctx context.Context) error {
@@ -203,7 +215,7 @@ func (b *preForkBlock) buildChild(ctx context.Context) (Block, error) {
// "chain is currently forking" path is the only one taken.
parentTimestamp := b.Timestamp()
parentID := b.ID()
newTimestamp := b.vm.Time().Truncate(time.Second)
newTimestamp := b.vm.Time().Truncate(proposer.TimestampGranularity())
if newTimestamp.Before(parentTimestamp) {
newTimestamp = parentTimestamp
}
@@ -275,6 +287,16 @@ func (b *preForkBlock) buildChild(ctx context.Context) (Block, error) {
}
// else: we ARE the elected proposer for this slot — build the unsigned block.
// The inner VM builds on ITS OWN head, and verifyPostForkChild below requires the
// transition block's inner parent to be exactly THIS block's inner block. Anchor the
// two together before delegating (see VM.anchorInnerBuildParent): at the fork height
// every validator may emit its own unsigned transition candidate, so a gossiped
// sibling that verifies here would otherwise drift the inner head and wedge the
// transition — the same self-rejection loop, at chain start.
if err := b.vm.anchorInnerBuildParent(ctx, parentID, b.Block.ID()); err != nil {
return nil, err
}
var innerBlock chain.Block
if b.vm.blockBuilderVM != nil {
// VM supports BuildBlockWithRuntime
+48 -10
View File
@@ -12,27 +12,65 @@ import (
"gonum.org/v1/gonum/mathext/prng"
validators "github.com/luxfi/validators"
"github.com/luxfi/container/sampler"
"github.com/luxfi/ids"
"github.com/luxfi/math"
"github.com/luxfi/utils"
validators "github.com/luxfi/validators"
)
// Proposer list constants
// Proposer list slot-COUNT constants (fixed; independent of the wall-clock
// window length).
const (
MaxVerifyWindows = 6
MaxBuildWindows = 60
MaxLookAheadSlots = 720
)
// WindowDuration is the proposer-slot spacing: a validator's minimum build delay
// is its slot index times WindowDuration. Default 5s (the avalanchego mainnet
// value, tuned for large validator sets). It is a startup-configurable VAR — small
// local/dev networks shrink it via SetWindowDuration (proposervm Config →
// --proposer-window-duration) so block cadence is not floored at 5s per proposer
// slot. It is read by BOTH the windower's delay math AND TimeToSlot, so the two
// stay consistent by construction.
var (
WindowDuration = 5 * time.Second
MaxVerifyWindows = 6
MaxVerifyDelay = MaxVerifyWindows * WindowDuration // 30 seconds
MaxBuildWindows = 60
MaxBuildDelay = MaxBuildWindows * WindowDuration // 5 minutes
MaxLookAheadSlots = 720
MaxLookAheadWindow = MaxLookAheadSlots * WindowDuration // 1 hour
MaxVerifyDelay = MaxVerifyWindows * WindowDuration // 30s at the default
MaxBuildDelay = MaxBuildWindows * WindowDuration // 5m at the default
MaxLookAheadWindow = MaxLookAheadSlots * WindowDuration // 1h at the default
)
// TimestampGranularity is the resolution block timestamps are truncated to. It
// tracks WindowDuration so the proposer-slot clock (TimeToSlot = elapsed /
// WindowDuration) and the block-timestamp clock advance at the SAME rate: with a
// coarser 1s truncation, a sub-second WindowDuration would inflate slot numbers
// without finer time resolution, so blocks could not actually be produced faster
// than 1/s. Capped at 1s so mainnet (WindowDuration >= 1s) keeps the original
// 1-second block-time granularity exactly.
func TimestampGranularity() time.Duration {
if WindowDuration > time.Second {
return time.Second
}
return WindowDuration
}
// SetWindowDuration overrides the proposer-slot spacing. It MUST be called at
// startup, before any chain begins block production: WindowDuration is read by
// both the windower delay math and TimeToSlot, so changing it mid-flight would
// desynchronize the slot mapping across the fleet (a consensus fault). Values
// <= 0 are ignored, so an unset config leaves the safe 5s default in place.
func SetWindowDuration(d time.Duration) {
if d <= 0 {
return
}
WindowDuration = d
MaxVerifyDelay = MaxVerifyWindows * WindowDuration
MaxBuildDelay = MaxBuildWindows * WindowDuration
MaxLookAheadWindow = MaxLookAheadSlots * WindowDuration
}
var (
_ Windower = (*windower)(nil)
+45
View File
@@ -0,0 +1,45 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package state
import (
"github.com/luxfi/database/prefixdb"
"github.com/luxfi/database/versiondb"
"github.com/luxfi/node/vms/proposervm/block"
)
// ScanBlocks visits every OUTER proposervm block persisted in the block store of
// [db], in key order.
//
// It is a REPAIR-PATH reader, deliberately kept OUT of the State interface: the
// hot path addresses blocks by id, and only the boot-time index rebuild
// (proposervm.rebuildOuterIndexFromStore) needs to enumerate them. Keeping it a
// free function over the same [blockStatePrefix] the store writes under means the
// prefix stays defined exactly once, and no mock/interface churn is imposed on
// callers that will never scan.
//
// Records that fail to decode are SKIPPED, not fatal: a rebuild must extract every
// usable wrapper from a store that may be partially damaged — that is the whole
// point of running it. Only an iterator-level error aborts the scan.
func ScanBlocks(db *versiondb.Database, visit func(block.Block) error) error {
blockDB := prefixdb.New(blockStatePrefix, db)
it := blockDB.NewIterator()
defer it.Release()
for it.Next() {
wrapper := blockWrapper{}
if err := parseBlockWrapper(it.Value(), &wrapper); err != nil {
continue
}
blk, err := block.ParseWithoutVerification(wrapper.Block)
if err != nil {
continue
}
if err := visit(blk); err != nil {
return err
}
}
return it.Error()
}
+183 -29
View File
@@ -121,6 +121,16 @@ type VM struct {
// [postForkBlock] and its inner block.
lastAcceptedTimestampGaugeVec metric.GaugeVec
// backfillMu guards [backfill]. It is a LEAF lock — never held across a
// callout — so it cannot deadlock against [lock], the [Tree] or the inner VM.
backfillMu sync.RWMutex
// backfill is non-nil ONLY while this node booted with a finality index that
// the local block store could not fully rebuild (see height_backfill.go). While
// it is set the chain runs read-only-ish: it refuses to BUILD blocks, and
// BackfillOuterBlock is the seam that completes the index. nil = index whole,
// which is the state of every healthy node.
backfill *outerBackfill
// quasarGate is the OPTIONAL post-quantum finality-cert gate. nil (the
// default) means PQ-finality verification is OFF — the accept path is
// unchanged classical Snow. When set AND forward-dated activation is reached,
@@ -289,6 +299,10 @@ func (vm *VM) Initialize(
// constants.PrimaryNetworkID, so a native chain matches the original
// proposer.New(..., PrimaryNetworkID, ...) byte-for-byte.
func (vm *VM) newWindower() proposer.Windower {
// Apply the configured proposer-slot spacing before the first schedule is
// resolved (no-op at the default; shrinks cadence on local/dev nets). Startup
// only — see proposer.SetWindowDuration.
proposer.SetWindowDuration(vm.Config.ProposerWindowDuration)
netID := vm.Config.NetworkID
if netID == ids.Empty {
netID = constants.PrimaryNetworkID
@@ -328,6 +342,18 @@ func (vm *VM) SetState(ctx context.Context, newState uint32) error {
}
func (vm *VM) BuildBlock(ctx context.Context) (vmchain.Block, error) {
// FAIL-SAFE while the finality index has a hole. A node that booted with an
// incomplete outer index (height_backfill.go) does not know the canonical
// envelope at its own tip, so anything it proposed would extend a parent the
// network does not recognise. It stays a follower until BackfillOuterBlock
// closes the gap. Serving RPC and following the chain are unaffected — this is
// exactly the difference between the old dead C-Chain and a live one.
if from, to, pending := vm.NeedsOuterBackfill(); pending {
return nil, fmt.Errorf(
"proposervm: refusing to build — finality index incomplete, outer envelopes for heights %d..%d are missing",
from, to)
}
preferredBlock, err := vm.getBlock(ctx, vm.preferred)
if err == nil {
return preferredBlock.buildChild(ctx)
@@ -379,6 +405,17 @@ func (vm *VM) BuildBlock(ctx context.Context) (vmchain.Block, error) {
}
func (vm *VM) ParseBlock(ctx context.Context, b []byte) (vmchain.Block, error) {
// SELF-HEAL SEAM. Every outer envelope this node receives — gossip, GetAncestors,
// catch-up — arrives here. If this node booted with a hole in its finality index
// (height_backfill.go), offer the bytes to the backfill: it takes ONLY the exact
// next missing height, only if the envelope verifies AND wraps the inner block we
// already accepted there, so a wrong or hostile block cannot land. That makes a
// damaged node repair itself from ordinary peer traffic with no new transport and
// no operator step. Errors are expected and ignored (almost every block is not the
// one we need); when nothing is pending this is one RLock and out.
if vm.outerBackfillPending() {
_ = vm.BackfillOuterBlock(ctx, b)
}
if blk, err := vm.parsePostForkBlock(ctx, b, true); err == nil {
return blk, nil
}
@@ -470,6 +507,56 @@ func (vm *VM) SetPreference(ctx context.Context, preferred ids.ID) error {
return nil
}
// anchorInnerBuildParent points the inner VM at [innerParentID] — the inner block wrapped
// by the outer parent a build is about to extend — and is called from both build
// delegations (postForkCommonComponents.buildChild and preForkBlock.buildChild) as the
// last step before asking the inner VM for a block.
//
// THE INVARIANT it establishes is the one the verify path enforces:
// child.innerBlk.Parent() == parent.innerBlk.ID(), else errInnerParentMismatch
// (block.go, and pre_fork_block.go for the transition block). The inner VM never reads
// the proposervm's parent — it builds on ITS OWN head (luxfi/evm: the miner reads
// bc.CurrentBlock()) — so build and verify read two different pointers, and the builder's
// is not ours to assume.
//
// WHY MAINTAINING IT IN SetPreference ALONE IS NOT ENOUGH. The inner head moves for
// reasons the proposervm never observes: verifying a GOSSIPED block whose parent is the
// current head optimistically makes it the head (evm core/blockchain.go
// writeBlockAndSetHead → newTip → writeCanonicalBlockWithLogs → writeHeadBlock), with no
// proposervm involvement and no accept. SetPreference cannot undo that drift either — it
// short-circuits on an unchanged outer preference (above), so re-affirming the same tip
// never re-pushes the inner preference. Every subsequent build then extends the drifted
// head, the node REJECTS THE BLOCK IT JUST BUILT, drops it, and repeats forever:
// devnet 96367 / testnet 96368 on 2026-07-28, "built block failed verification —
// dropping / inner parentID didn't match expected parent", 83456 drops/min per node,
// the accepted tip frozen while the builder ran two heights ahead of it. Never
// self-corrects, on any node, ever.
//
// SetPreference on the INNER VM is the right primitive and the only one: it is that VM's
// own head-reorg entry point (evm VM.SetPreference → BlockChain.SetPreference →
// writeKnownBlock, which performs the reorg side effects). Asserting it at the point of
// use — rather than trusting a distant caller to have done it — is what makes the
// requirement local to the code that depends on it.
//
// COST AND SAFETY. On a healthy node the head already IS the parent's inner block and the
// inner SetPreference returns early on that identity (evm setPreference:
// `current.Hash() == block.Hash()`), so this is one lookup and no state change — zero
// behaviour change in the common case. When it fails, the head is provably NOT the
// parent's inner block, so any child built would fail this node's own Verify; refusing to
// build is strictly better than emitting a block we are guaranteed to drop.
func (vm *VM) anchorInnerBuildParent(ctx context.Context, parentID, innerParentID ids.ID) error {
if err := vm.ChainVM.SetPreference(ctx, innerParentID); err != nil {
vm.logger.Error("unexpected build block failure",
log.String("reason", "failed to anchor the inner VM on the parent's inner block"),
log.Stringer("parentID", parentID),
log.Stringer("innerParentID", innerParentID),
log.Err(err),
)
return fmt.Errorf("failed to anchor inner build parent %s: %w", innerParentID, err)
}
return nil
}
func (vm *VM) WaitForEvent(ctx context.Context) (vmcore.Message, error) {
for {
if err := ctx.Err(); err != nil {
@@ -547,7 +634,7 @@ func (vm *VM) timeToBuild(ctx context.Context) (time.Time, bool, error) {
parentTimestamp = blk.Timestamp()
nextStartTime time.Time
)
currentTime := vm.Clock.Time().Truncate(time.Second)
currentTime := vm.Clock.Time().Truncate(proposer.TimestampGranularity())
if nextStartTime, err = vm.getPostDurangoSlotTime(
ctx,
childBlockHeight,
@@ -598,7 +685,7 @@ func (vm *VM) timeToBuildPreForkTransitionLocked(ctx context.Context) (time.Time
var (
parentTimestamp = pre.Timestamp()
childHeight = pre.Height() + 1
currentTime = vm.Clock.Time().Truncate(time.Second)
currentTime = vm.Clock.Time().Truncate(proposer.TimestampGranularity())
slot = proposer.TimeToSlot(parentTimestamp, currentTime)
)
// MinDelayForProposer returns the delay until THIS node's earliest slot in the
@@ -779,35 +866,51 @@ func (vm *VM) repairAcceptedChainByHeight(ctx context.Context) error {
return nil
case heightBehind:
// INVARIANT VIOLATION and UNRECOVERABLE LOCALLY: the proposervm's finality
// index sits BELOW the inner VM's accepted tip. In a correct system this
// never happens — the proposervm last-accepted pointer and height index
// commit in the SAME versiondb batch as every inner accept, so they cannot
// lag. Reaching here means the on-disk proposervm state was truncated
// relative to the inner EVM — e.g. a snapshot restored inconsistently across
// the two databases (the devnet-C "index 7 < inner 8").
// INVARIANT VIOLATION: the proposervm's finality index sits BELOW the inner
// VM's accepted tip. Every post-fork accept commits the envelope, its height
// entry and the last-accepted pointer in ONE versiondb batch BEFORE the inner
// block is accepted, so this can only mean some accept advanced the inner VM
// WITHOUT going through acceptPostForkBlock (the pre-fork fallback — now
// prevented, see height_backfill.go) or that the on-disk proposervm state was
// truncated relative to the inner EVM.
//
// We FAIL LOUD rather than "self-heal", because there is no correct local
// heal: the proposervm cannot fabricate the missing outer wrapper blocks for
// heights (pro, inner]. In particular, dropping the finality pointer
// (DeleteLastAccepted) is NOT a heal — proposervm.LastAccepted() would then
// fall back to the inner-namespace id (see LastAccepted), whose ParentID is
// contiguity-incompatible with the network's OUTER wrappers, permanently
// wedging bootstrap (the first-block anchor), catch-up (the parent==tip
// guard) and live Verify (the parent lookup) at the inner tip — and since
// every path skips blocks at height <= the tip, the missing wrapper is never
// rebuilt. That silent wedge is strictly worse than this loud, actionable
// stop. The correct remedy is operator action; surface it explicitly.
return fmt.Errorf(
"proposervm finality index (height %d, id %s) is BEHIND the inner VM tip (height %d, id %s): "+
"the on-disk proposervm state is truncated/inconsistent relative to the inner EVM "+
"(e.g. a snapshot restored inconsistently across the proposervm and EVM databases). "+
"This cannot be repaired locally — the proposervm cannot rebuild the missing outer wrapper "+
"blocks. RECOVERY: restore a snapshot that is consistent across BOTH databases, or fully "+
"resync this node from peers (wipe this chain's db and re-bootstrap). Refusing to auto-reset "+
"the finality pointer, which would silently wedge this node at the inner tip forever",
proLastAcceptedHeight, proLastAcceptedID, innerLastAcceptedHeight, innerLastAcceptedID,
// This used to be a hard init failure, which killed the chain on the node
// ("non-critical chain failed to initialize chainAlias=C") and made every
// restart fatal. It is now REPAIRED — outer-only, with no inner
// re-execution:
//
// 1. re-derive the index from the outer envelopes already in this node's
// block store, each bound to the inner block WE accepted at that height;
// 2. if that cannot reach the tip, START ANYWAY in an explicit, loud,
// build-gated backfill-pending state that BackfillOuterBlock completes.
//
// The finality pointer is only ever moved FORWARD onto a proven envelope and
// is NEVER dropped: a DeleteLastAccepted here would make LastAccepted() fall
// back to an inner-namespace id whose ParentID is contiguity-incompatible
// with the network's outer wrappers, silently wedging bootstrap/catch-up/live
// Verify at the inner tip forever.
vm.logger.Warn("proposervm finality index is BEHIND the inner VM tip — repairing",
log.Uint64("indexHeight", proLastAcceptedHeight),
log.Stringer("indexID", proLastAcceptedID),
log.Uint64("innerTipHeight", innerLastAcceptedHeight),
log.Stringer("innerTipID", innerLastAcceptedID),
)
reached, err := vm.rebuildOuterIndexFromStore(
ctx, proLastAcceptedHeight, proLastAcceptedID, innerLastAcceptedHeight)
if err != nil {
return err
}
if reached >= innerLastAcceptedHeight {
vm.logger.Info("proposervm finality index REBUILT from the local block store — index and inner tip agree",
log.Uint64("fromHeight", proLastAcceptedHeight),
log.Uint64("toHeight", reached),
)
return nil
}
vm.enterOuterBackfill(reached+1, innerLastAcceptedHeight, innerLastAcceptedID)
return nil
}
// heightAhead: the inner vm is BEHIND the proposer vm (the inner rolled back or
@@ -1024,16 +1127,67 @@ func (vm *VM) getPreForkBlock(ctx context.Context, blkID ids.ID) (*preForkBlock,
if err != nil {
return nil, err
}
// ROOT-CAUSE GUARD (the finality-index lag). getBlock() falls back here
// whenever [blkID] is not an OUTER envelope id — and the id the consensus
// ledger records as canonical IS the inner block's id
// (postForkCommonComponents.CanonicalID). Without this check, asking the
// proposervm for a canonical id post-fork silently returns a preForkBlock
// wrapping a post-fork inner block, whose Accept advances the inner VM and
// leaves the outer index untouched (preForkBlock.acceptOuterBlk is a no-op).
// That is how the index ends up BEHIND the inner tip while everything looks
// healthy, and why the NEXT boot could not start the chain.
//
// Post-fork, a block at or above the recorded fork height is by definition NOT
// a pre-fork block: there is no such thing as a legitimate pre-fork block at a
// height the proposervm has already claimed. Refuse to construct it.
if err := vm.refusePreForkAfterFork(engineBlk.Height()); err != nil {
return nil, err
}
return &preForkBlock{
Block: engineBlk,
vm: vm,
}, nil
}
// refusePreForkAfterFork returns errPreForkAfterFork when [height] is at or above
// the recorded proposervm fork height. Before the fork (no fork height recorded)
// every block is legitimately pre-fork and this is a no-op, so a chain that has
// not yet transitioned is completely unaffected. ONE predicate, used by both the
// construction guard (getPreForkBlock) and the accept guard
// (preForkBlock.acceptOuterBlk), so the two can never disagree.
func (vm *VM) refusePreForkAfterFork(height uint64) error {
forkHeight, err := vm.State.GetForkHeight()
if err == database.ErrNotFound {
return nil // chain has not forked; everything is pre-fork
}
if err != nil {
return err
}
if height < forkHeight {
return nil
}
return fmt.Errorf("%w: height %d >= fork height %d", errPreForkAfterFork, height, forkHeight)
}
func (vm *VM) acceptPostForkBlock(blk PostForkBlock) error {
height := blk.Height()
blkID := blk.ID()
// EARLY DETECTOR for the finality-index lag. The index must advance one height
// at a time; a jump means some accept moved the inner VM without moving the
// index (the pre-fork fallback the guards above now refuse) and the gap will be
// fatal at the next boot. Surfacing it HERE names the height where the hole
// opened instead of leaving a post-mortem for the next restart. Not fatal: the
// accept itself is correct and the boot-time rebuild can close a hole; refusing
// finality on a warning would be the worse trade.
if vm.lastAcceptedHeight != 0 && height != vm.lastAcceptedHeight+1 {
vm.logger.Warn("proposervm finality index is NOT contiguous — a height was accepted without being indexed",
log.Uint64("previousIndexedHeight", vm.lastAcceptedHeight),
log.Uint64("acceptedHeight", height),
log.Stringer("blkID", blkID),
)
}
vm.lastAcceptedHeight = height
vm.forgetVerifiedBlock(blkID)
+299
View File
@@ -0,0 +1,299 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// vm_crashboot_test.go — CRASH-COPY RECOVERY ARMOR.
//
// Every recovery test before this file repaired the SAME warm VM value: its
// Tree, verifiedBlocks map and caches were still populated, so a repair that
// silently leaned on in-memory state would still have passed. These tests
// remove that crutch. They copy the COMMITTED bytes out from under a RUNNING
// proposervm — no Shutdown, no extra Commit beyond what the accept path itself
// performed — and boot a SECOND, cold VM over the copy: a process kill plus
// restart, byte-faithful. The recovered VM must
//
// 1. BOOT — repairAcceptedChainByHeight then setLastAcceptedMetadata, the
// Initialize order — without error;
// 2. AGREE with the source's committed view at the copy instant: finality
// pointer, fork height, and an openable envelope at EVERY height; and
// 3. BUILD — produce a child whose outer parent is the recovered tip and
// whose inner parent is that tip's inner block. This is the
// errInnerParentMismatch invariant (build_inner_parent_test.go) proven
// from a cold boot on persisted state, not just at steady state.
//
// The matrix pins the persistence boundaries: nothing accepted yet, exactly
// one accept (the fork-height record), a longer run, and a copy taken while
// the source keeps accepting (the copy must be an immutable snapshot).
// TestCrashBoot_OuterCommittedInnerNot pins the ONE window the accept path
// leaves open: acceptPostForkBlock commits the outer batch BEFORE the inner
// VM accepts, so a crash between the two strands the index one ahead — boot
// must roll the pointer back onto the inner survivor and keep building.
//
// The shared harness lives in height_lag_repro_test.go.
package proposervm
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/luxfi/database"
"github.com/luxfi/database/memdb"
"github.com/luxfi/ids"
validatorstest "github.com/luxfi/validators/validatorstest"
vmchain "github.com/luxfi/vm/chain"
"github.com/luxfi/vm/chain/blocktest"
)
// crashCopy copies every committed key/value out of a live base database into
// a fresh memdb — a byte-level snapshot taken with the source still running.
// This is exactly what a crash leaves behind: versiondb buffers uncommitted
// writes in memory, and they die with the process.
func crashCopy(t *testing.T, base database.Database) *memdb.Database {
t.Helper()
cp := memdb.New()
it := base.NewIterator()
defer it.Release()
for it.Next() {
if err := cp.Put(it.Key(), it.Value()); err != nil {
t.Fatalf("crashCopy Put: %v", err)
}
}
if err := it.Error(); err != nil {
t.Fatalf("crashCopy iterator: %v", err)
}
return cp
}
// snapshot clones the inner chain as its own database would survive a crash at
// this instant: every block ever inserted (verified-but-unaccepted included,
// as the real EVM persists bodies on insert) with the acceptance tip pinned at
// [tip]. The clone is immune to the source's subsequent progress.
func (ic *innerChain) snapshot(tip uint64) *innerChain {
cp := &innerChain{
byHeight: make(map[uint64]*blocktest.Block, len(ic.byHeight)),
byID: make(map[ids.ID]*blocktest.Block, len(ic.byID)),
byBytes: make(map[string]*blocktest.Block, len(ic.byBytes)),
tip: tip,
}
for h, b := range ic.byHeight {
cp.byHeight[h] = b
}
for id, b := range ic.byID {
cp.byID[id] = b
}
for s, b := range ic.byBytes {
cp.byBytes[s] = b
}
return cp
}
// buildableVM is ic.vm() plus the two calls a build needs. SetPreference is
// anchorInnerBuildParent's seam — it moves the head exactly as the EVM does.
// BuildBlock mints a FRESH child of the head (building is not accepting, and a
// replacement proposal after a crash is a new block, not the lost one).
func (ic *innerChain) buildableVM() *blocktest.VM {
vm := ic.vm()
vm.SetPreferenceF = func(_ context.Context, id ids.ID) error {
blk, ok := ic.byID[id]
if !ok {
return errors.New("inner: preference not in chain")
}
ic.tip = blk.HeightV
return nil
}
vm.BuildBlockF = func(context.Context) (vmchain.Block, error) {
parent := ic.byHeight[ic.tip]
id := ids.GenerateTestID()
child := &blocktest.Block{
IDV: id,
ParentV: parent.IDV,
HeightV: parent.HeightV + 1,
BytesV: append([]byte("inner"), id[:]...),
TimestampV: parent.TimestampV.Add(time.Second),
}
ic.byID[id] = child
ic.byBytes[string(child.BytesV)] = child
return child, nil
}
return vm
}
// bootRecoveredVM stands a COLD VM up over copied bytes and runs the boot
// sequence in Initialize order. Windower/validator-state/no-op wiring matches
// build_inner_parent_test.go: every slot open, so builds take the unsigned
// path and the recovery properties are the only thing under test.
func bootRecoveredVM(t *testing.T, ic *innerChain, base database.Database) *VM {
t.Helper()
vm := testVMOnBase(t, ic, base)
vm.ChainVM = ic.buildableVM()
vm.Windower = anyoneCanProposeWindower{}
vm.validatorState = &validatorstest.State{
GetCurrentHeightF: func(context.Context) (uint64, error) { return 0, nil },
}
if err := vm.repairAcceptedChainByHeight(context.Background()); err != nil {
t.Fatalf("BOOT KILLED THE CHAIN (repair): %v", err)
}
if err := vm.setLastAcceptedMetadata(context.Background()); err != nil {
t.Fatalf("BOOT KILLED THE CHAIN (metadata): %v", err)
}
return vm
}
// mustBuildOnRecoveredTip drives the recovered VM's REAL public build path —
// SetPreference(last accepted) then BuildBlock, what the engine does after a
// restart — and asserts the built child extends the recovered tip on BOTH
// layers: outer parent == the tip envelope, inner parent == the tip's inner
// block. A recovered node failing either would reject its own proposal.
func mustBuildOnRecoveredTip(t *testing.T, vm *VM, ic *innerChain) {
t.Helper()
require := require.New(t)
ctx := context.Background()
// Deterministic clock strictly after the recovered tip's timestamp
// (harness blocks carry time.Unix(height, 0)).
vm.Clock.Set(time.Unix(int64(ic.tip)+2, 0))
lastAccepted, err := vm.LastAccepted(ctx)
require.NoError(err)
require.NoError(vm.SetPreference(ctx, lastAccepted),
"the recovered tip must be fetchable — SetPreference validates before assigning")
built, err := vm.BuildBlock(ctx)
require.NoError(err, "a crash-recovered node must be able to build")
child, ok := built.(*postForkBlock)
require.True(ok, "the child of a recovered tip is a post-fork envelope")
wantInnerParent := ic.byHeight[ic.tip].IDV
require.Equal(wantInnerParent, child.innerBlk.Parent(),
"the built child's inner parent must be the recovered tip's inner block "+
"(errInnerParentMismatch invariant, from a cold boot)")
require.Equal(ic.tip+1, child.Height())
require.Equal(lastAccepted, child.Parent(),
"the built child's outer parent must be the recovered finality pointer")
}
// TestCrashBoot_ColdVMOverDirtyCopy_AgreesAndBuilds is the persistence-boundary
// matrix: commit-state copies taken with NO shutdown, each booted by a second,
// cold VM that must agree with the source's committed view at the copy instant
// and then build on it.
func TestCrashBoot_ColdVMOverDirtyCopy_AgreesAndBuilds(t *testing.T) {
for _, tt := range []struct {
name string
accepted uint64 // heights accepted through the proposervm before the copy
runOnAfter uint64 // heights the SOURCE keeps accepting after the copy
}{
// Nothing ever accepted through the proposervm: no fork height, no
// outer state. Boot must be a no-op and the pre-fork world intact.
{name: "nothing_accepted", accepted: 0, runOnAfter: 0},
// Exactly one accept — the boundary that records the fork height.
{name: "first_accept_records_fork", accepted: 1, runOnAfter: 0},
{name: "several_accepted", accepted: 8, runOnAfter: 0},
// The copy is taken and the source RUNS ON: later accepts must not
// leak into the recovered view (the copy is an immutable snapshot).
{name: "copy_while_source_runs_on", accepted: 5, runOnAfter: 3},
} {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)
ctx := context.Background()
ic := newInnerChain(t, tt.accepted+tt.runOnAfter+2)
base := memdb.New()
src := testVMOnBase(t, ic, base)
outers := acceptThroughProposervm(t, src, ic, tt.accepted)
// THE COPY — no Shutdown, the source stays live. Snapshot both
// layers at the same instant, as one machine's disk would be.
cp := crashCopy(t, base)
icAtCopy := ic.snapshot(ic.tip)
if tt.runOnAfter > 0 {
acceptRangeThroughProposervm(
t, src, ic, tt.accepted+1, tt.accepted+tt.runOnAfter, outers[tt.accepted].ID())
if got := indexHeight(t, src); got != tt.accepted+tt.runOnAfter {
t.Fatalf("source must have run on to %d, at %d", tt.accepted+tt.runOnAfter, got)
}
}
vm2 := bootRecoveredVM(t, icAtCopy, cp)
if tt.accepted == 0 {
// Pre-fork world: no outer state exists, none may be invented.
_, err := vm2.State.GetLastAccepted()
require.ErrorIs(err, database.ErrNotFound)
_, err = vm2.State.GetForkHeight()
require.ErrorIs(err, database.ErrNotFound)
// LastAccepted falls through to the inner chain.
la, err := vm2.LastAccepted(ctx)
require.NoError(err)
require.Equal(icAtCopy.byHeight[icAtCopy.tip].IDV, la)
} else {
// The recovered finality pointer is the copy-instant tip —
// not genesis, not the source's later progress.
got, err := vm2.State.GetLastAccepted()
require.NoError(err)
require.Equal(outers[tt.accepted].ID(), got)
require.Equal(mustForkHeight(t, src), mustForkHeight(t, vm2))
// Every height's envelope is indexed AND openable cold.
for h := uint64(1); h <= tt.accepted; h++ {
id, err := vm2.State.GetBlockIDAtHeight(h)
require.NoError(err, "height %d must be indexed", h)
require.Equal(outers[h].ID(), id, "height %d", h)
blk, err := vm2.getPostForkBlock(ctx, id)
require.NoError(err, "envelope at height %d must be openable on the recovered VM", h)
require.Equal(h, blk.Height())
}
}
if _, _, pending := vm2.NeedsOuterBackfill(); pending {
t.Fatal("a lock-step copy has no gap — no backfill may be pending")
}
mustBuildOnRecoveredTip(t, vm2, icAtCopy)
})
}
}
// TestCrashBoot_OuterCommittedInnerNot_RollsBackAndBuilds pins the one crash
// window the accept path leaves open. acceptPostForkBlock commits the envelope,
// height index and finality pointer in ONE batch BEFORE the inner VM accepts
// (that ordering is why the index can never legitimately fall behind). A crash
// inside the window therefore persists an index one AHEAD of the inner
// survivor. Boot must take repairAcceptedChainByHeight's roll-back arm — move
// the pointer back onto the height the inner VM survived at — and the node
// must then build its replacement proposal there.
func TestCrashBoot_OuterCommittedInnerNot_RollsBackAndBuilds(t *testing.T) {
require := require.New(t)
ctx := context.Background()
ic := newInnerChain(t, 8)
base := memdb.New()
src := testVMOnBase(t, ic, base)
outers := acceptThroughProposervm(t, src, ic, 4)
require.Equal(uint64(4), ic.tip, "precondition: lock-step through 4")
// Height 5's outer accept COMMITS…
sb := outerFor(t, ic, 5, outers[4].ID())
blk := &postForkBlock{
SignedBlock: sb,
postForkCommonComponents: postForkCommonComponents{vm: src, innerBlk: ic.byHeight[5]},
}
src.Tree.Add(ic.byHeight[5])
require.NoError(blk.Accept(ctx))
// …and the process dies BEFORE the inner VM accepts 5 (no ic.accept(5)).
cp := crashCopy(t, base)
icAtCopy := ic.snapshot(4)
vm2 := bootRecoveredVM(t, icAtCopy, cp)
got, err := vm2.State.GetLastAccepted()
require.NoError(err)
require.Equal(outers[4].ID(), got,
"boot must roll the finality pointer back onto the height the inner VM survived at")
// The node re-proposes at 5: a NEW envelope over a NEW inner child of 4 —
// the lost block is gone, not resurrected.
mustBuildOnRecoveredTip(t, vm2, icAtCopy)
}
@@ -17,7 +17,7 @@ import (
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/go-bip32"
"github.com/luxfi/go-bip39"
"github.com/luxfi/keys"
"github.com/luxfi/kms/pkg/mnemonic"
)
const (
@@ -31,10 +31,10 @@ const (
// MustLoadKey loads a secp256k1 private key from (in order of priority):
// 1. MNEMONIC environment variable (BIP-39 mnemonic phrase)
// 2. via native ZAP — when KMS_ADDR + KMS_ENV +
// 2. via native ZAP — when KMS_ADDR + KMS_ENV +
// KMS_MNEMONIC_PATH are set in the environment. Uses the canonical
// luxfi/kms keys.LoadMnemonic loader so every Lux-derived
// service resolves keys the same way.
// kms/pkg/mnemonic loader so every Lux-derived service resolves
// keys the same way.
// 3. Key name provided as first command-line argument
// 4. ~/.lux/keys/default/ if it exists
//
@@ -58,28 +58,31 @@ func MustLoadKey() *secp256k1.PrivateKey {
// LoadKey attempts to load a private key using the priority order above.
func LoadKey() (*secp256k1.PrivateKey, error) {
// 1. MNEMONIC env var — local dev + CI test seam.
if mnemonic := strings.TrimSpace(os.Getenv("MNEMONIC")); mnemonic != "" {
return keyFromMnemonic(mnemonic)
if phrase := strings.TrimSpace(os.Getenv("MNEMONIC")); phrase != "" {
return keyFromMnemonic(phrase)
}
// 2. via native ZAP — production path for any Lux-derived
// service running under a KMS-projected env. The canonical loader
// lives in luxfi/keys (alongside the BIP-39 derivation primitives)
// so luxd, netrunner, lux/cli, and every descending L1's bootstrap
// all resolve mnemonics the same way.
// Trust at the network boundary (NetworkPolicy + ZAP wire) — the
// staking material itself comes from the KMS-held operational
// mnemonic at KMS_MNEMONIC_PATH (default /mnemonic).
// lives in kms/pkg/mnemonic, not luxfi/keys: keys must not import
// kms, because kms already imports keys for ServiceIdentity, and
// the back edge would close an import cycle. luxd, netrunner,
// lux/cli, and every descending L1's bootstrap all resolve
// mnemonics through it.
// Trust at the network boundary (NetworkPolicy + ZAP wire) — hence
// a nil ServiceIdentity — the staking material itself comes from
// the KMS-held operational mnemonic at KMS_MNEMONIC_PATH.
if addr := os.Getenv("KMS_ADDR"); addr != "" {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
mnemonic, err := keys.LoadMnemonicFromKMS(ctx, addr,
phrase, err := mnemonic.LoadFromKMS(ctx, addr,
os.Getenv("KMS_ENV"),
envOr("KMS_MNEMONIC_PATH", "/mnemonic"))
envOr("KMS_MNEMONIC_PATH", "/mnemonic"),
nil)
if err != nil {
return nil, fmt.Errorf("load mnemonic from KMS: %w", err)
}
return keyFromMnemonic(mnemonic)
return keyFromMnemonic(phrase)
}
// 3. Key name from command-line arguments.